From 62ad7755f3aaa172d6869a73a5339c8a39a4cd03 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Wed, 11 Jan 2023 21:37:30 +0530 Subject: [PATCH 001/352] New context structure changes --- backend/src/api/derived/config.rs | 66 +++- backend/src/api/derived/context_override.rs | 10 +- backend/src/api/primary/context_overrides.rs | 21 +- backend/src/api/primary/dimensions.rs | 8 +- backend/src/api/primary/global_config.rs | 79 ++--- backend/src/api/primary/mod.rs | 1 + backend/src/api/primary/new_contexts.rs | 305 ++++++++++++++++++ backend/src/api/primary/overrides.rs | 42 +-- backend/src/db/schema.rs | 38 ++- backend/src/handlers/mod.rs | 1 + backend/src/handlers/new_contexts.rs | 66 ++++ backend/src/main.rs | 13 +- backend/src/messages/mod.rs | 1 + backend/src/messages/new_contexts.rs | 31 ++ backend/src/models/db_models.rs | 15 + backend/src/models/insertables/mod.rs | 1 + .../src/models/insertables/new_contexts.rs | 16 + backend/src/utils/helpers.rs | 14 +- backend/src/utils/validations.rs | 4 + 19 files changed, 621 insertions(+), 111 deletions(-) create mode 100644 backend/src/api/primary/new_contexts.rs create mode 100644 backend/src/handlers/new_contexts.rs create mode 100644 backend/src/messages/new_contexts.rs create mode 100644 backend/src/models/insertables/new_contexts.rs diff --git a/backend/src/api/derived/config.rs b/backend/src/api/derived/config.rs index f2b7c4e78..970d3e66c 100644 --- a/backend/src/api/derived/config.rs +++ b/backend/src/api/derived/config.rs @@ -12,8 +12,9 @@ use serde_json::{to_value, Error, Value}; use crate::api::primary::{ context_overrides::fetch_override_from_ctx_id, - global_config::get_complete_config, + // global_config::get_complete_config, contexts::fetch_context, + new_contexts::fetch_new_contexts, overrides::get_override_helper, }; @@ -29,6 +30,7 @@ use crate::utils::{ helpers::{ create_all_unique_subsets, split_stringified_key_value_pair, + strip_double_quotes }, }; @@ -43,7 +45,7 @@ fn default_parsing_error(err: Error) -> AppError{ } -async fn get_context_overrides_object(state: &Data, query_string: &str) -> Result { +async fn _get_context_overrides_object(state: &Data, query_string: &str) -> Result { if query_string == "" { return Ok(Value::default()); } @@ -109,23 +111,67 @@ async fn get_context_overrides_object(state: &Data, query_string: &str } +async fn get_new_context_overrides_object (state: &Data, query_string: &str) -> Result { + let raw_contexts = fetch_new_contexts(state, query_string.to_string()).await?; -#[get("")] -pub async fn get_config(state: Data, req: HttpRequest) -> Result, AppError> { - let query_string = req.query_string(); + let mut contexts = Vec::new(); + let mut override_map = HashMap::new(); + + for item in raw_contexts { + + match (item.get("id"), item.get("condition")) { + (Some(context_id), Some(condition)) => { + + if let Ok(override_id) = + fetch_override_from_ctx_id( + &state, + strip_double_quotes(&context_id.to_string()) + ).await { + + let fetched_override_value = get_override_helper(&state, override_id.to_owned()).await?; + + override_map.insert(override_id.to_owned(), fetched_override_value); + + contexts.push( + to_value(HashMap::from([ + ("overrideWithKeys", &to_value(override_id).map_err(default_parsing_error)?), + ("condition", condition), + ])) + .map_err(|err| AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + })? + ); + } + + }, + (_, _) => (), + } + } - let global_config = to_value(get_complete_config(&state).await?).map_err(|err| AppError { + to_value(HashMap::from([ + ("context", to_value(contexts).map_err(default_parsing_error)?), + ("overrides", to_value(override_map).map_err(default_parsing_error)?), + ])) + .map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), status: DBError, - })?; + }) - let context_overrides = get_context_overrides_object(&state, query_string).await?; +} + + +#[get("")] +pub async fn get_config(state: Data, req: HttpRequest) -> Result, AppError> { + let query_string = req.query_string(); Ok(Json(to_value(HashMap::from([ - ("global_config", global_config), - ("context_overrides", context_overrides) + // ("global_config", get_complete_config(&state).await?), + // ("context_overrides", get_context_overrides_object(&state, query_string).await?) + ("context_overrides", get_new_context_overrides_object(&state, query_string).await?) ])).map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), diff --git a/backend/src/api/derived/context_override.rs b/backend/src/api/derived/context_override.rs index f3f434a71..1e55b83bb 100644 --- a/backend/src/api/derived/context_override.rs +++ b/backend/src/api/derived/context_override.rs @@ -20,8 +20,9 @@ use crate::utils::errors::{ use crate::api::primary::{ overrides::add_new_override, - contexts::add_new_context, + // contexts::add_new_context, context_overrides::add_ctx_override, + new_contexts::add_new_context_v2 }; @@ -45,12 +46,13 @@ pub async fn add_new_context_override(state: Data, body: Json, context_id: String, override_id: String) -> Result, AppError> { +pub async fn add_ctx_override(state: &Data, context_id: String, override_id: String, return_if_present: bool) -> Result, AppError> { let db: Addr = state.db.clone(); match db - .send(CreateCtxOverrides {context_id, override_id}) + .send(CreateCtxOverrides {context_id: context_id.to_owned(), override_id}) .await { Ok(Ok(result)) => Ok(Json(ContextOverrideResponse {context_id: result.context_id})), - Ok(Err(err)) => Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), + Ok(Err(err)) => + if return_if_present { + Ok(Json(ContextOverrideResponse {context_id})) + } else { + Err(AppError { + message: Some("Data already exists".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists + }) + }, Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } @@ -80,7 +85,7 @@ pub async fn fetch_override_from_ctx_id(state: &Data, context_id: &str pub async fn post_ctx_override(state: Data, body: Json) -> Result, AppError> { let ctx_id: String = body.context_id.clone(); let ovr_id : String = body.override_id.clone(); - add_ctx_override(&state, ctx_id, ovr_id).await + add_ctx_override(&state, ctx_id, ovr_id, false).await } #[get("/{id}")] diff --git a/backend/src/api/primary/dimensions.rs b/backend/src/api/primary/dimensions.rs index f07bd0369..01812ce90 100644 --- a/backend/src/api/primary/dimensions.rs +++ b/backend/src/api/primary/dimensions.rs @@ -28,10 +28,14 @@ use crate::utils::errors::{ // Get dimension table #[get("")] pub async fn get_dimensions(state: Data) -> Result>, AppError> { - let db: Addr = state.as_ref().db.clone(); + fetch_dimensions(&state).await.map(Json) +} + +pub async fn fetch_dimensions(state: &Data) -> Result, AppError> { + let db: Addr = state.db.clone(); match db.send(FetchDimensions).await { - Ok(Ok(result)) => Ok(Json(result)), + Ok(Ok(result)) => Ok(result), Ok(Err(err)) => Err(AppError { message: Some("failed to get dimensions".to_string()), cause: Some(Left(err.to_string())), diff --git a/backend/src/api/primary/global_config.rs b/backend/src/api/primary/global_config.rs index 9dd15880d..fde706be4 100644 --- a/backend/src/api/primary/global_config.rs +++ b/backend/src/api/primary/global_config.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::collections::{ + BTreeMap, + HashMap +}; use actix_web:: { Either::{Left}, @@ -6,7 +9,8 @@ use actix_web:: { post, web::{Path, Json, Data}, }; -use serde_json::{Value, to_value}; +use log::info; +use serde_json::{Value, from_value, to_value}; use crate::models::db_models::GlobalConfig; use serde::{Serialize, Deserialize}; use crate::{ @@ -43,7 +47,7 @@ async fn get_all_rows_from_global_config(state: &Data) -> Result) -> Result, AppError> { +pub async fn get_complete_config(state: &Data) -> Result { let db_rows = get_all_rows_from_global_config(&state).await?; let mut hash_map: HashMap = HashMap::new(); @@ -51,30 +55,19 @@ pub async fn get_complete_config(state: &Data) -> Result, hash_map.insert(row.key, row.value); } - match to_value(&hash_map) { - Ok(res) => if hash_map.keys().len() == 0 { - Err(AppError { - message: Some("failed to get global config".to_string()), - cause: Some(Left("global config doesn't exist".to_string())), - status: NotFound - }) - } else { - Ok(Json(res)) - }, - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - }) - - } + to_value(&hash_map) + .map_err(|err| AppError { + message: Some("Unable to fetch global config".to_string()), + cause: Some(Left(err.to_string())), + status: SomethingWentWrong + }) } // Get whole global config #[get("")] pub async fn get_global_config(state: Data) -> Result, AppError> { - get_complete_config(&state).await + Ok(Json(get_complete_config(&state).await?)) } // Get request to fetch value for given key @@ -100,32 +93,30 @@ pub async fn get_global_config_key(state: Data, params: Path) -> } -// Post request to add key, value -#[derive(Deserialize, Serialize)] -pub struct KeyValue { - key: String, - value: Value, -} - #[post("")] -pub async fn post_config_key_value(state: Data, body: Json) -> Result, AppError> { +pub async fn post_config_key_value(state: Data, body: Json) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); - match db.send(CreateGlobalKey { - key: body.key.clone(), - value: body.value.clone() - }).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to add new key to global config".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) + let b_tree: BTreeMap = + from_value(body.clone()) + .map_err(|err| AppError {message: None, cause: Some(Left(err.to_string())), status: DBError})?; + + for (key, value) in b_tree { + info!("Key value pair {} {:?}", key, value); + match db.send(CreateGlobalKey { key, value }).await { + Ok(Ok(result)) => Ok(Json(result)), + Ok(Err(err)) => Err(AppError { + message: Some("Failed to add new key to global config".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError + }) + }?; } + Ok(body) } diff --git a/backend/src/api/primary/mod.rs b/backend/src/api/primary/mod.rs index e7a452044..95f5a38b8 100644 --- a/backend/src/api/primary/mod.rs +++ b/backend/src/api/primary/mod.rs @@ -3,3 +3,4 @@ pub mod dimensions; pub mod overrides; pub mod contexts; pub mod context_overrides; +pub mod new_contexts; \ No newline at end of file diff --git a/backend/src/api/primary/new_contexts.rs b/backend/src/api/primary/new_contexts.rs new file mode 100644 index 000000000..60d9ad5d0 --- /dev/null +++ b/backend/src/api/primary/new_contexts.rs @@ -0,0 +1,305 @@ +use std::{ + cmp::Ordering, + collections::HashMap +}; + +use actix::Addr; +use actix_web::{ + Either::{Left}, + get, + post, + web::{Data, Json}, + HttpRequest, +}; +use serde::Serialize; +use serde_json::{from_value, to_value, Error, Value}; + +use crate::{ + messages::new_contexts::{CreateNewContext, FetchNewContext}, + api::primary::{ + dimensions::fetch_dimensions + }, + AppState, DbActor +}; + +use crate::models::db_models::NewContexts; + +use crate::utils::{ + errors::{ + AppError, + AppErrorType::{ + DataExists, + NotFound, + DBError, + SomethingWentWrong + } + }, + hash::string_based_b64_hash, + helpers::{ + split_stringified_key_value_pair, + strip_double_quotes, + }, +}; + +#[derive(Serialize, Clone)] +pub struct ContextIdResponse { + pub id: String, +} + +fn default_parsing_error(err: Error) -> AppError { + AppError { + message: None, + cause: Some(Left(err.to_string())), + status: SomethingWentWrong + } +} + +fn get_dimension_name (idx: usize) -> String { + let dimesions = Vec::from(["tier", "merchantId", "os", "country"]); + dimesions[idx].to_string() +} + +fn contexts_comparator(priority_map: HashMap) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { + + let get_value_from_map = move |key: Option| { + + if let Some(val) = key { + if let Some(val1) = priority_map.get(&val) { + return val1.to_owned(); + } + } + + return 0; + }; + + move |ctx1: &NewContexts, ctx2: &NewContexts| { + let val1 = + get_value_from_map(ctx1.column1.to_owned().map(|_| get_dimension_name(0))) + + get_value_from_map(ctx1.column2.to_owned().map(|_| get_dimension_name(1))) + + get_value_from_map(ctx1.column3.to_owned().map(|_| get_dimension_name(2))) + + get_value_from_map(ctx1.column4.to_owned().map(|_| get_dimension_name(3))); + + let val2 = + get_value_from_map(ctx2.column1.to_owned().map(|_| get_dimension_name(0))) + + get_value_from_map(ctx2.column2.to_owned().map(|_| get_dimension_name(1))) + + get_value_from_map(ctx2.column3.to_owned().map(|_| get_dimension_name(2))) + + get_value_from_map(ctx2.column4.to_owned().map(|_| get_dimension_name(3))); + + val1.cmp(&val2) + } +} + + +pub async fn fetch_raw_context_v2( + state: &Data, + column1: Option<&String>, + column2: Option<&String>, + column3: Option<&String>, + column4: Option<&String>, + dimesion_map: HashMap +) -> Result, AppError> { + + let db: Addr = state.db.clone(); + + let result: Vec = match db + .send(FetchNewContext { + column1: column1.map(|x| x.to_string()), + column2: column2.map(|x| x.to_string()), + column3: column3.map(|x| x.to_string()), + column4: column4.map(|x| x.to_string()), + }) + .await + { + Ok(Ok(res)) => Ok(res), + Ok(Err(err)) => Err(AppError { + message: Some("Failed to get context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError + }), + }?; + + + let mut final_result: Vec = + result + .into_iter() + .filter(|y| { + let x = y.to_owned(); + let column1_from_row: Option = x.column1.to_owned(); + let column2_from_row: Option = x.column2.to_owned(); + let column3_from_row: Option = x.column3.to_owned(); + let column4_from_row: Option = x.column4.to_owned(); + + + (column1_from_row.is_none() || column1.is_none() || column1_from_row == column1.map(|x| x.to_string())) && + (column2_from_row.is_none() || column2.is_none() || column2_from_row == column2.map(|x| x.to_string())) && + (column3_from_row.is_none() || column3.is_none() || column3_from_row == column3.map(|x| x.to_string())) && + (column4_from_row.is_none() || column4.is_none() || column4_from_row == column4.map(|x| x.to_string())) + }) + .collect(); + + final_result.sort_by(contexts_comparator(dimesion_map)); + + Ok(final_result) + +} + + +pub fn process_single_condition(value_object: &HashMap) -> HashMap { + + let mut result_map: HashMap = HashMap::new(); + + // Range check have to be implemented + if value_object.contains_key("==") { + let variable_array_value = value_object.get("==").unwrap(); + let variable_array: Vec = from_value(variable_array_value.to_owned()).unwrap(); + + if variable_array.len() >= 2 { + let variable_map: HashMap = from_value(variable_array[0].to_owned()).unwrap(); + let mapped_value = variable_array[1].to_string(); + + if let Some(key) = variable_map.get("var") { + let key_string = key.to_string(); + + result_map.insert( + // Removing double quotes at the start and end for both key and value + strip_double_quotes(&key_string).to_owned(), + strip_double_quotes(&mapped_value).to_owned() + ); + } + } + } + + result_map +} + +pub fn process_input_context_json(input_json: &Value) -> HashMap { + + let input_as_map: HashMap = from_value(input_json.to_owned()).unwrap(); // map_err(default_parsing_error)?; + + if !input_as_map.contains_key("and") { + return process_single_condition(&input_as_map); + } + + let mut result_map: HashMap = HashMap::new(); + let val = input_as_map.get("and").unwrap(); + let multiple_condition_array: Vec = from_value(val.to_owned()).unwrap(); + + + for item in multiple_condition_array { + let value_object: HashMap = from_value(item).unwrap(); // .map_err(default_parsing_error)?; + result_map.extend(process_single_condition(&value_object)); + } + + result_map +} + +pub async fn add_new_context_v2(state: &Data, context_value: Value, return_if_present: bool) -> Result { + let db: Addr = state.db.clone(); + + let key_value_map = process_input_context_json(&context_value); + + // ? TODO :: Post as an array of value + // ? TODO :: Sort query based on key and add to DB + // let ss = Json(&context_value); + // let hashed_value = "test12".to_string(); + + let hashed_value = string_based_b64_hash(context_value.to_string()).to_string(); + + match db + .send(CreateNewContext { + key: hashed_value.to_owned(), + value: context_value, + column1: key_value_map.get(&get_dimension_name(0)).map(|x| x.to_string()), + column2: key_value_map.get(&get_dimension_name(1)).map(|x| x.to_string()), + column3: key_value_map.get(&get_dimension_name(2)).map(|x| x.to_string()), + column4: key_value_map.get(&get_dimension_name(3)).map(|x| x.to_string()), + }) + .await + { + Ok(Ok(result)) => Ok(ContextIdResponse {id: result.key}), + Ok(Err(err)) => + if return_if_present { + Ok(ContextIdResponse {id: hashed_value}) + } else { + Err(AppError { + message: Some("Failed to add context".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists + }) + }, + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError + }), + } +} + +pub async fn fetch_new_contexts(state: &Data, query_string: String) -> Result>, AppError> { + let key_value_pairs = split_stringified_key_value_pair(&query_string); + + let all_dimesions = fetch_dimensions(&state).await?; + + let mut dimesion_map = HashMap::new(); + + for item in all_dimesions { + dimesion_map.insert(item.dimension, item.priority); + } + + let mut filter_input_keys_map = HashMap::new(); + + for item in key_value_pairs { + if dimesion_map.contains_key(item[0]) { + filter_input_keys_map.insert(item[0].to_string(), item[1].to_string()); + } + } + + let raw_contexts = + fetch_raw_context_v2( + &state, + filter_input_keys_map.get(&get_dimension_name(0)), + filter_input_keys_map.get(&get_dimension_name(1)), + filter_input_keys_map.get(&get_dimension_name(2)), + filter_input_keys_map.get(&get_dimension_name(3)), + dimesion_map + ).await?; + + let mut formatted_contexts = Vec::new(); + for item in raw_contexts { + formatted_contexts.push( + HashMap::from([ + ("condition", item.value), + ("id", to_value(item.key).map_err(default_parsing_error)?), + ]) + ); + } + + Ok(formatted_contexts) +} + + +#[get("")] +pub async fn get_new_context(state: Data, req: HttpRequest) -> Result, AppError> { + let query_string = req.query_string(); + let formatted_contexts = fetch_new_contexts(&state, query_string.to_string()).await?; + let mut result = Vec::new(); + + for item in &formatted_contexts { + result.push(HashMap::from([ + ("condition", item.get("condition").to_owned()) + ])); + } + + Ok(Json(to_value(result).map_err(default_parsing_error)?)) +} + +#[post("")] +pub async fn post_new_context(state: Data, body: Json) -> Result, AppError> { + let context_value = body.clone(); + Ok(Json(add_new_context_v2(&state, context_value, false).await?)) +} diff --git a/backend/src/api/primary/overrides.rs b/backend/src/api/primary/overrides.rs index 58562a713..222c19e6a 100644 --- a/backend/src/api/primary/overrides.rs +++ b/backend/src/api/primary/overrides.rs @@ -7,7 +7,7 @@ use actix_web::{ Either::{Left, Right} }; use serde::Serialize; -use serde_json::{Value, to_value}; +use serde_json::{Value}; use crate::{ messages::overrides::{CreateOverride, DeleteOverride, FetchOverride}, @@ -38,26 +38,10 @@ pub struct OverrideIdResponse { } -pub async fn add_new_override(state: &Data, override_value: Value) -> Result { +pub async fn add_new_override(state: &Data, override_value: Value, return_if_present: bool) -> Result { let db: Addr = state.db.clone(); - let global_config = - get_complete_config(&state).await - .map_err(|err| AppError { - message: Some("Unable to fetch global config for validation".to_string()), - cause: Some(Left(err.to_string())), - status: DBError - })?; - - - let global_config_as_value = - // TODO :: Discuss and fix this - to_value(global_config.get("global")) - .map_err(|err| AppError { - message: Some("Unable to parse global config for validation".to_string()), - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - })?; + let global_config_as_value = get_complete_config(&state).await?; if let Err(error_message) = validate_sub_tree(&global_config_as_value, &override_value) { return Err(AppError { @@ -67,7 +51,6 @@ pub async fn add_new_override(state: &Data, override_value: Value) -> }) } - // TODO :: Post as an array of value let formatted_value = sort_multi_level_keys_in_stringified_json(override_value) @@ -83,17 +66,22 @@ pub async fn add_new_override(state: &Data, override_value: Value) -> match db .send(CreateOverride { - key: hashed_value, + key: hashed_value.to_owned(), value: formatted_value, }) .await { Ok(Ok(result)) => Ok(OverrideIdResponse {id: result.key}), - Ok(Err(err)) => Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), + Ok(Err(err)) => + if return_if_present { + Ok(OverrideIdResponse {id: hashed_value}) + } else { + Err(AppError { + message: Some("Data already exists".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists + }) + }, Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } @@ -101,7 +89,7 @@ pub async fn add_new_override(state: &Data, override_value: Value) -> #[post("")] pub async fn post_override(state: Data, body: Json) -> Result, AppError> { let override_value = body.clone(); - Ok(Json(add_new_override(&state, override_value).await?)) + Ok(Json(add_new_override(&state, override_value, false).await?)) } pub async fn get_override_helper(state: &Data, key: String) -> Result, AppError> { diff --git a/backend/src/db/schema.rs b/backend/src/db/schema.rs index 33e66e2fc..8fd4da13d 100644 --- a/backend/src/db/schema.rs +++ b/backend/src/db/schema.rs @@ -1,45 +1,58 @@ // @generated automatically by Diesel CLI. diesel::table! { - contexts (key) { - key -> Varchar, - value -> Varchar, + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - ctxoverrides (context_id) { - context_id -> Varchar, - override_id -> Varchar, + global_config (key) { + key -> Varchar, + value -> Json, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, + overrides (key) { + key -> Varchar, + value -> Json, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - global_config (key) { + contexts (key) { key -> Varchar, - value -> Json, + value -> Varchar, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - overrides (key) { + newcontexts (key) { key -> Varchar, value -> Json, + column1 -> Nullable, + column2 -> Nullable, + column3 -> Nullable, + column4 -> Nullable, + last_modified -> Timestamptz, + created_on -> Timestamptz, + } +} + +diesel::table! { + ctxoverrides (context_id) { + context_id -> Varchar, + override_id -> Varchar, last_modified -> Timestamptz, created_on -> Timestamptz, } @@ -51,4 +64,5 @@ diesel::allow_tables_to_appear_in_same_query!( dimensions, global_config, overrides, + newcontexts ); diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index a79c4e1a4..f26d16026 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -3,3 +3,4 @@ pub mod global_config; pub mod overrides; pub mod contexts; pub mod context_overrides; +pub mod new_contexts; \ No newline at end of file diff --git a/backend/src/handlers/new_contexts.rs b/backend/src/handlers/new_contexts.rs new file mode 100644 index 000000000..bfefa2a43 --- /dev/null +++ b/backend/src/handlers/new_contexts.rs @@ -0,0 +1,66 @@ +use diesel::QueryResult; + +use crate::db::utils::DbActor; +use crate::models::db_models::NewContexts; + +use crate::db::schema::newcontexts::dsl::*; +use crate::messages::new_contexts::{CreateNewContext, FetchNewContext, DeleteNewContext}; +use actix::Handler; +use diesel::{self, prelude::*}; + +use crate::models::insertables::new_contexts::NewContextInsertion; + +impl Handler for DbActor { + type Result = QueryResult; + + fn handle(&mut self, msg: CreateNewContext, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for creating context override"); + + diesel::insert_into(newcontexts) + .values(NewContextInsertion { + key : msg.key, + value : msg.value, + column1: msg.column1, + column2: msg.column2, + column3: msg.column3, + column4: msg.column4, + }) + .get_result::(&mut conn) + } +} + +impl Handler for DbActor { + type Result = QueryResult>; + + fn handle(&mut self, _msg: FetchNewContext, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + + // ! TODO :: Move filter directly over here (DB scanning) + newcontexts.get_results::(&mut conn) + // .filter( + + // (column1 + // .eq(msg.column1) + // .or(column1.is_null()) + // ) + + // .and( + // column2 + // .eq(msg.column2) + // .or(column2.is_null()) + // ) + // ) + } +} + +impl Handler for DbActor { + type Result = QueryResult; + + fn handle(&mut self, _msg: DeleteNewContext, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + + diesel::delete(newcontexts) + .filter(column1.eq("".to_string())) + .get_result::(&mut conn) + } +} \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index 263caba77..b1016eb86 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -31,7 +31,11 @@ use api::primary::{ post_ctx_override, delete_ctx_override, get_ctx_override, - } + }, + new_contexts::{ + get_new_context, + post_new_context, + }, }; use api::derived::{ @@ -91,11 +95,16 @@ async fn main() -> Result<()> { .service(delete_override) .service(get_override) ).service( - scope("/context") + scope("/oldcontext") .service(post_context) .service(delete_context) .service(get_context) + ).service( + scope("/context") + .service(get_new_context) + .service(post_new_context) + ) /***************************** Derived api routes *****************************/ diff --git a/backend/src/messages/mod.rs b/backend/src/messages/mod.rs index a79c4e1a4..f26d16026 100644 --- a/backend/src/messages/mod.rs +++ b/backend/src/messages/mod.rs @@ -3,3 +3,4 @@ pub mod global_config; pub mod overrides; pub mod contexts; pub mod context_overrides; +pub mod new_contexts; \ No newline at end of file diff --git a/backend/src/messages/new_contexts.rs b/backend/src/messages/new_contexts.rs new file mode 100644 index 000000000..20c445483 --- /dev/null +++ b/backend/src/messages/new_contexts.rs @@ -0,0 +1,31 @@ + +use actix::Message; +use serde_json::Value; +use crate::models::db_models::NewContexts; +use diesel::QueryResult; + +#[derive(Message)] +#[rtype(result="QueryResult")] +pub struct CreateNewContext { + pub key: String, + pub value: Value, + pub column1: Option, + pub column2: Option, + pub column3: Option, + pub column4: Option, +} + +#[derive(Message)] +#[rtype(result="QueryResult>")] +pub struct FetchNewContext { + pub column1: Option, + pub column2: Option, + pub column3: Option, + pub column4: Option, +} + +#[derive(Message)] +#[rtype(result="QueryResult")] +pub struct DeleteNewContext { + pub key: String, +} diff --git a/backend/src/models/db_models.rs b/backend/src/models/db_models.rs index 56c88cd10..58f8622a4 100644 --- a/backend/src/models/db_models.rs +++ b/backend/src/models/db_models.rs @@ -56,3 +56,18 @@ pub struct CtxOverrides { pub last_modified: DateTime, pub created_on: DateTime, } + + +#[derive(Queryable, Debug, Identifiable, Serialize)] +#[diesel(table_name = contexts)] +#[diesel(primary_key(key))] +pub struct NewContexts { + pub key: String, + pub value: Value, + pub column1: Option, + pub column2: Option, + pub column3: Option, + pub column4: Option, + pub last_modified: DateTime, + pub created_on: DateTime, +} \ No newline at end of file diff --git a/backend/src/models/insertables/mod.rs b/backend/src/models/insertables/mod.rs index a79c4e1a4..f26d16026 100644 --- a/backend/src/models/insertables/mod.rs +++ b/backend/src/models/insertables/mod.rs @@ -3,3 +3,4 @@ pub mod global_config; pub mod overrides; pub mod contexts; pub mod context_overrides; +pub mod new_contexts; \ No newline at end of file diff --git a/backend/src/models/insertables/new_contexts.rs b/backend/src/models/insertables/new_contexts.rs new file mode 100644 index 000000000..1067dae34 --- /dev/null +++ b/backend/src/models/insertables/new_contexts.rs @@ -0,0 +1,16 @@ +use diesel::Insertable; +use serde::Serialize; +use serde_json::Value; + +use crate::db::schema::newcontexts; + +#[derive(Debug, Insertable, Serialize)] +#[diesel(table_name = newcontexts)] +pub struct NewContextInsertion { + pub key : String, + pub value: Value, + pub column1: Option, + pub column2: Option, + pub column3: Option, + pub column4: Option, +} diff --git a/backend/src/utils/helpers.rs b/backend/src/utils/helpers.rs index 480436ed7..b420fc305 100644 --- a/backend/src/utils/helpers.rs +++ b/backend/src/utils/helpers.rs @@ -6,7 +6,7 @@ pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { to_value(b_tree).ok() } -fn create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { +fn _create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { let mut n = idx; let mut vector_index = 0; let mut result: Vec = Vec::new(); @@ -21,7 +21,7 @@ fn create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { return result; } -pub fn create_all_unique_subsets(s: &Vec<&str>) -> Vec> { +pub fn _create_all_unique_subsets(s: &Vec<&str>) -> Vec> { let mut res: Vec> = Vec::new(); for i in 1..(1 << s.len()) { res.push(create_all_unique_subsets_helper(s ,i)); @@ -40,4 +40,14 @@ pub fn split_stringified_key_value_pair(input: &str) -> Vec> { conditions_vector.sort_by(|a, b| a[0].cmp(&b[0])); return conditions_vector; +} + +pub fn strip_double_quotes (str: &str) -> &str { + + if str.starts_with("\"") && str.ends_with("\"") { + let len = str.len(); + return &str[1..len-1]; + } + + str } \ No newline at end of file diff --git a/backend/src/utils/validations.rs b/backend/src/utils/validations.rs index 2b64726a4..809f29cda 100644 --- a/backend/src/utils/validations.rs +++ b/backend/src/utils/validations.rs @@ -27,6 +27,10 @@ fn type_of(a: &Value) -> String{ return "Array".to_string(); } + if a.is_null() { + return "Null".to_string(); + } + return "Object".to_string(); } From 84389aeeb63bba32b24d45f85f8747d5e14e2227 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Wed, 11 Jan 2023 21:39:43 +0530 Subject: [PATCH 002/352] Compilation fix --- backend/src/utils/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/utils/helpers.rs b/backend/src/utils/helpers.rs index b420fc305..62876690d 100644 --- a/backend/src/utils/helpers.rs +++ b/backend/src/utils/helpers.rs @@ -6,7 +6,7 @@ pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { to_value(b_tree).ok() } -fn _create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { +fn create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { let mut n = idx; let mut vector_index = 0; let mut result: Vec = Vec::new(); @@ -21,7 +21,7 @@ fn _create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { return result; } -pub fn _create_all_unique_subsets(s: &Vec<&str>) -> Vec> { +pub fn create_all_unique_subsets(s: &Vec<&str>) -> Vec> { let mut res: Vec> = Vec::new(); for i in 1..(1 << s.len()) { res.push(create_all_unique_subsets_helper(s ,i)); From b0d92dff1e1ec718e70946912bd543ea757c7238 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Wed, 11 Jan 2023 21:40:41 +0530 Subject: [PATCH 003/352] Added global config in config api result --- backend/src/api/derived/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/derived/config.rs b/backend/src/api/derived/config.rs index 970d3e66c..df9beeaf5 100644 --- a/backend/src/api/derived/config.rs +++ b/backend/src/api/derived/config.rs @@ -12,7 +12,7 @@ use serde_json::{to_value, Error, Value}; use crate::api::primary::{ context_overrides::fetch_override_from_ctx_id, - // global_config::get_complete_config, + global_config::get_complete_config, contexts::fetch_context, new_contexts::fetch_new_contexts, overrides::get_override_helper, @@ -169,7 +169,7 @@ pub async fn get_config(state: Data, req: HttpRequest) -> Result Date: Thu, 12 Jan 2023 00:52:19 +0530 Subject: [PATCH 004/352] Reduce api added --- backend/src/api/derived/mod.rs | 3 +- backend/src/api/derived/reduce.rs | 79 ++++++++++++++++++++ backend/src/api/primary/context_overrides.rs | 23 +++++- backend/src/api/primary/global_config.rs | 2 - backend/src/api/primary/new_contexts.rs | 47 +++++++++++- backend/src/api/primary/overrides.rs | 33 ++++++-- backend/src/handlers/context_overrides.rs | 11 ++- backend/src/handlers/new_contexts.rs | 15 +++- backend/src/handlers/overrides.rs | 11 ++- backend/src/main.rs | 9 ++- backend/src/messages/context_overrides.rs | 4 + backend/src/messages/new_contexts.rs | 5 ++ backend/src/messages/overrides.rs | 4 + 13 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 backend/src/api/derived/reduce.rs diff --git a/backend/src/api/derived/mod.rs b/backend/src/api/derived/mod.rs index dfd77c76b..ccabfcb2d 100644 --- a/backend/src/api/derived/mod.rs +++ b/backend/src/api/derived/mod.rs @@ -1,2 +1,3 @@ pub mod config; -pub mod context_override; \ No newline at end of file +pub mod context_override; +pub mod reduce; \ No newline at end of file diff --git a/backend/src/api/derived/reduce.rs b/backend/src/api/derived/reduce.rs new file mode 100644 index 000000000..26411b84f --- /dev/null +++ b/backend/src/api/derived/reduce.rs @@ -0,0 +1,79 @@ +// TODO :: Handle errors with appropriate error message +use std::collections::HashSet; + +use actix_web::{ + delete, + web::{Data, Json}, +}; + +use crate::utils::errors::AppError; + +use crate::api::primary::{ + overrides::{delete_override_helper, get_all_overrides}, + context_overrides::fetch_all_ctx_overrides, + new_contexts::{delete_new_context_helper, fetch_all_new_contexts} +}; + + +use crate::{AppState}; + +use std::iter::FromIterator; + + +fn find_diff(all_keys: &Vec, sub_set_keys: &Vec) -> Vec { + + let a_set: HashSet = HashSet::from_iter(sub_set_keys.iter().cloned()); + let mut difference = Vec::new(); + + for i in all_keys { + if !a_set.contains(i) { + difference.push(i.to_owned()); + } + } + + difference +} + +#[delete("")] +pub async fn reduce_contexts_overrides(input_state: Data) -> Result, AppError> { + let state = &input_state; + + let ctx_overrides = fetch_all_ctx_overrides(state).await?; + + let mapped_context_ids: Vec = + ctx_overrides + .iter() + .map(|i| i.context_id.to_owned()) + .collect(); + + let mapped_override_ids: Vec = + ctx_overrides + .iter() + .map(|i| i.override_id.to_owned()) + .collect(); + + + let all_context_ids: Vec = + fetch_all_new_contexts(state).await? + .iter() + .map(|x| x.key.to_owned()) + .collect(); + let all_override_ids: Vec = + get_all_overrides(state).await? + .iter() + .map(|x| x.key.to_owned()) + .collect(); + + let extra_context_ids = find_diff(&all_context_ids, &mapped_context_ids); + let extra_override_ids = find_diff(&all_override_ids, &mapped_override_ids); + + for item in extra_context_ids { + delete_new_context_helper(&state, item).await?; + } + + for item in extra_override_ids { + delete_override_helper(&state, item).await?; + } + + Ok(Json("Okay".to_string())) +} \ No newline at end of file diff --git a/backend/src/api/primary/context_overrides.rs b/backend/src/api/primary/context_overrides.rs index 6e78347b1..f075d6aee 100644 --- a/backend/src/api/primary/context_overrides.rs +++ b/backend/src/api/primary/context_overrides.rs @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{to_value, Value}; use crate::{ - messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchCtxOverrides}, - AppState, DbActor, + messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides}, + AppState, DbActor, models::db_models::CtxOverrides, }; use crate::utils::{ @@ -81,6 +81,23 @@ pub async fn fetch_override_from_ctx_id(state: &Data, context_id: &str } } +pub async fn fetch_all_ctx_overrides(state: &Data) -> Result, AppError> { + let db: Addr = state.as_ref().db.clone(); + + match db + .send(FetchAllCtxOverrides) + .await + { + Ok(Ok(result)) => Ok(result), + Ok(Err(err)) => Err(AppError { + message: Some("failed to fetch key value".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound + }), + Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) + } +} + #[post("")] pub async fn post_ctx_override(state: Data, body: Json) -> Result, AppError> { let ctx_id: String = body.context_id.clone(); @@ -119,7 +136,7 @@ pub async fn delete_ctx_override(state: Data, id: Path) -> Res { Ok(Ok(result)) => Ok(Json(serde_json::Value::String(result.context_id))), Ok(Err(err)) => Err(AppError { - message: Some("Data not found".to_string()), + message: Some("Data not found for context override deletion".to_string()), cause: Some(Left(err.to_string())), status: NotFound }), diff --git a/backend/src/api/primary/global_config.rs b/backend/src/api/primary/global_config.rs index fde706be4..04fe385a0 100644 --- a/backend/src/api/primary/global_config.rs +++ b/backend/src/api/primary/global_config.rs @@ -9,7 +9,6 @@ use actix_web:: { post, web::{Path, Json, Data}, }; -use log::info; use serde_json::{Value, from_value, to_value}; use crate::models::db_models::GlobalConfig; use serde::{Serialize, Deserialize}; @@ -102,7 +101,6 @@ pub async fn post_config_key_value(state: Data, body: Json) -> .map_err(|err| AppError {message: None, cause: Some(Left(err.to_string())), status: DBError})?; for (key, value) in b_tree { - info!("Key value pair {} {:?}", key, value); match db.send(CreateGlobalKey { key, value }).await { Ok(Ok(result)) => Ok(Json(result)), Ok(Err(err)) => Err(AppError { diff --git a/backend/src/api/primary/new_contexts.rs b/backend/src/api/primary/new_contexts.rs index 60d9ad5d0..899cf9243 100644 --- a/backend/src/api/primary/new_contexts.rs +++ b/backend/src/api/primary/new_contexts.rs @@ -6,16 +6,17 @@ use std::{ use actix::Addr; use actix_web::{ Either::{Left}, + delete, get, post, - web::{Data, Json}, + web::{Data, Json, Path}, HttpRequest, }; use serde::Serialize; use serde_json::{from_value, to_value, Error, Value}; use crate::{ - messages::new_contexts::{CreateNewContext, FetchNewContext}, + messages::new_contexts::{CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext}, api::primary::{ dimensions::fetch_dimensions }, @@ -240,6 +241,42 @@ pub async fn add_new_context_v2(state: &Data, context_value: Value, re } } +pub async fn fetch_all_new_contexts(state: &Data) -> Result, AppError> { + let db: Addr = state.db.clone(); + + match db.send(FetchAllNewContexts).await { + Ok(Ok(res)) => Ok(res), + Ok(Err(err)) => Err(AppError { + message: Some("Failed to get context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError + }), + } +} + +pub async fn delete_new_context_helper(state: &Data, key: String) -> Result { + let db: Addr = state.db.clone(); + + match db.send(DeleteNewContext {key}).await { + Ok(Ok(res)) => Ok(res), + Ok(Err(err)) => Err(AppError { + message: Some("Failed to delete context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError + }), + } +} + pub async fn fetch_new_contexts(state: &Data, query_string: String) -> Result>, AppError> { let key_value_pairs = split_stringified_key_value_pair(&query_string); @@ -303,3 +340,9 @@ pub async fn post_new_context(state: Data, body: Json) -> Resul let context_value = body.clone(); Ok(Json(add_new_context_v2(&state, context_value, false).await?)) } + + +#[delete("/{id}")] +pub async fn delete_new_context(state: Data, id: Path) -> Result, AppError> { + Ok(Json(delete_new_context_helper(&state, id.to_string()).await?)) +} diff --git a/backend/src/api/primary/overrides.rs b/backend/src/api/primary/overrides.rs index 222c19e6a..d75501140 100644 --- a/backend/src/api/primary/overrides.rs +++ b/backend/src/api/primary/overrides.rs @@ -10,8 +10,8 @@ use serde::Serialize; use serde_json::{Value}; use crate::{ - messages::overrides::{CreateOverride, DeleteOverride, FetchOverride}, - AppState, DbActor, + messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}, + AppState, DbActor, models::db_models::Overrides, }; use crate::utils::{ @@ -109,14 +109,30 @@ pub async fn get_override_helper(state: &Data, key: String) -> Result< } } +pub async fn get_all_overrides(state: &Data) -> Result, AppError> { + let db: Addr = state.db.clone(); + + match db + .send(FetchAllOverrides) + .await + { + Ok(Ok(result)) => Ok(result), + Ok(Err(err)) => Err(AppError { + message: Some("Failed to fetch value for given override key".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound + }), + Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) + } +} + #[get("/{key}")] pub async fn get_override(state: Data, key: Path) -> Result, AppError> { get_override_helper(&state, key.to_owned()).await } -#[delete("/{key}")] -pub async fn delete_override(state: Data, id: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); +pub async fn delete_override_helper(state: &Data, id: String) -> Result, AppError> { + let db: Addr = state.db.clone(); match db .send(DeleteOverride { @@ -126,10 +142,15 @@ pub async fn delete_override(state: Data, id: Path) -> Result< { Ok(Ok(result)) => Ok(Json(result.value)), Ok(Err(err)) => Err(AppError { - message: Some("Data not found".to_string()), + message: Some("Data not found for override deletion".to_string()), cause: Some(Left(err.to_string())), status: NotFound }), Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } + +#[delete("/{key}")] +pub async fn delete_override(state: Data, id: Path) -> Result, AppError> { + delete_override_helper(&state, id.to_string()).await +} \ No newline at end of file diff --git a/backend/src/handlers/context_overrides.rs b/backend/src/handlers/context_overrides.rs index a717a9a24..2b14bb96c 100644 --- a/backend/src/handlers/context_overrides.rs +++ b/backend/src/handlers/context_overrides.rs @@ -4,7 +4,7 @@ use crate::db::utils::DbActor; use crate::models::db_models::CtxOverrides; use crate::db::schema::ctxoverrides::dsl::*; -use crate::messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchCtxOverrides}; +use crate::messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides}; use actix::Handler; use diesel::{self, prelude::*}; @@ -37,6 +37,15 @@ impl Handler for DbActor { } } +impl Handler for DbActor { + type Result = QueryResult>; + + fn handle(&mut self, _msg: FetchAllCtxOverrides, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + ctxoverrides.get_results::(&mut conn) + } +} + impl Handler for DbActor { type Result = QueryResult; diff --git a/backend/src/handlers/new_contexts.rs b/backend/src/handlers/new_contexts.rs index bfefa2a43..d3d6e0d15 100644 --- a/backend/src/handlers/new_contexts.rs +++ b/backend/src/handlers/new_contexts.rs @@ -4,7 +4,7 @@ use crate::db::utils::DbActor; use crate::models::db_models::NewContexts; use crate::db::schema::newcontexts::dsl::*; -use crate::messages::new_contexts::{CreateNewContext, FetchNewContext, DeleteNewContext}; +use crate::messages::new_contexts::{CreateNewContext, FetchAllNewContexts, FetchNewContext, DeleteNewContext}; use actix::Handler; use diesel::{self, prelude::*}; @@ -53,14 +53,23 @@ impl Handler for DbActor { } } +impl Handler for DbActor { + type Result = QueryResult>; + + fn handle(&mut self, _msg: FetchAllNewContexts, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + newcontexts.get_results::(&mut conn) + } +} + impl Handler for DbActor { type Result = QueryResult; - fn handle(&mut self, _msg: DeleteNewContext, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: DeleteNewContext, _: &mut Self::Context) -> Self::Result { let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); diesel::delete(newcontexts) - .filter(column1.eq("".to_string())) + .filter(key.eq(msg.key)) .get_result::(&mut conn) } } \ No newline at end of file diff --git a/backend/src/handlers/overrides.rs b/backend/src/handlers/overrides.rs index 1825dba15..d7b90fc74 100644 --- a/backend/src/handlers/overrides.rs +++ b/backend/src/handlers/overrides.rs @@ -4,7 +4,7 @@ use crate::db::utils::DbActor; use crate::models::db_models::Overrides; use crate::db::schema::overrides::dsl::*; -use crate::messages::overrides::{CreateOverride, DeleteOverride, FetchOverride}; +use crate::messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}; use actix::Handler; use diesel::{self, prelude::*}; @@ -37,6 +37,15 @@ impl Handler for DbActor { } } +impl Handler for DbActor { + type Result = QueryResult>; + + fn handle(&mut self, _msg: FetchAllOverrides, _: &mut Self::Context) -> Self::Result { + let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + overrides.get_results::(&mut conn) + } +} + impl Handler for DbActor { type Result = QueryResult; diff --git a/backend/src/main.rs b/backend/src/main.rs index b1016eb86..767e6b7d8 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -33,6 +33,7 @@ use api::primary::{ get_ctx_override, }, new_contexts::{ + delete_new_context, get_new_context, post_new_context, }, @@ -40,7 +41,8 @@ use api::primary::{ use api::derived::{ config::get_config, - context_override::add_new_context_override + context_override::add_new_context_override, + reduce::reduce_contexts_overrides, }; // use crate::utils::validations::just_for_test; @@ -104,6 +106,7 @@ async fn main() -> Result<()> { scope("/context") .service(get_new_context) .service(post_new_context) + .service(delete_new_context) ) @@ -116,6 +119,10 @@ async fn main() -> Result<()> { scope("add_context_overrides") .service(add_new_context_override) ) + .service( + scope("reduce") + .service(reduce_contexts_overrides) + ) }) .bind(("localhost", 8080))? .workers(5) diff --git a/backend/src/messages/context_overrides.rs b/backend/src/messages/context_overrides.rs index 793d636a7..02d480931 100644 --- a/backend/src/messages/context_overrides.rs +++ b/backend/src/messages/context_overrides.rs @@ -10,6 +10,10 @@ pub struct CreateCtxOverrides { pub override_id: String } +#[derive(Message)] +#[rtype(result="QueryResult>")] +pub struct FetchAllCtxOverrides; + #[derive(Message)] #[rtype(result="QueryResult")] pub struct FetchCtxOverrides { diff --git a/backend/src/messages/new_contexts.rs b/backend/src/messages/new_contexts.rs index 20c445483..f25375a8a 100644 --- a/backend/src/messages/new_contexts.rs +++ b/backend/src/messages/new_contexts.rs @@ -15,6 +15,11 @@ pub struct CreateNewContext { pub column4: Option, } + +#[derive(Message)] +#[rtype(result="QueryResult>")] +pub struct FetchAllNewContexts; + #[derive(Message)] #[rtype(result="QueryResult>")] pub struct FetchNewContext { diff --git a/backend/src/messages/overrides.rs b/backend/src/messages/overrides.rs index 9c101a5d8..a5adec965 100644 --- a/backend/src/messages/overrides.rs +++ b/backend/src/messages/overrides.rs @@ -11,6 +11,10 @@ pub struct CreateOverride { pub value: Value, } +#[derive(Message)] +#[rtype(result="QueryResult>")] +pub struct FetchAllOverrides; + #[derive(Message)] #[rtype(result="QueryResult")] pub struct FetchOverride { From c03d772660f9b9c27b7639d72e0723a233d187f4 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Fri, 3 Feb 2023 15:22:16 +0530 Subject: [PATCH 005/352] Promote api added --- backend/src/api/derived/mod.rs | 3 +- backend/src/api/derived/promote.rs | 65 +++++++++++++++++++++++++ backend/src/api/derived/reduce.rs | 2 +- backend/src/api/primary/new_contexts.rs | 55 ++++++++++++++------- backend/src/main.rs | 5 ++ 5 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 backend/src/api/derived/promote.rs diff --git a/backend/src/api/derived/mod.rs b/backend/src/api/derived/mod.rs index ccabfcb2d..dfa6f48b8 100644 --- a/backend/src/api/derived/mod.rs +++ b/backend/src/api/derived/mod.rs @@ -1,3 +1,4 @@ pub mod config; pub mod context_override; -pub mod reduce; \ No newline at end of file +pub mod reduce; +pub mod promote; \ No newline at end of file diff --git a/backend/src/api/derived/promote.rs b/backend/src/api/derived/promote.rs new file mode 100644 index 000000000..182e2f338 --- /dev/null +++ b/backend/src/api/derived/promote.rs @@ -0,0 +1,65 @@ +// TODO :: Handle errors with appropriate error message +use actix_web::{ + Either::Left, + post, + web::{Data, Json}, +}; +use serde_json::{to_value, Error, Value}; +use serde::{Serialize, Deserialize}; + +use crate::api::primary::{ + context_overrides::{ContextOverrideResponse, add_ctx_override, fetch_override_from_ctx_id}, + new_contexts::{add_new_context_v2, process_input_context_json} +}; +use crate::AppState; +use crate::utils::{ + errors::{ + AppError, + AppErrorType::{ + SomethingWentWrong + } + }, + hash::string_based_b64_hash +}; + +fn default_parsing_error(err: Error) -> AppError { + AppError { + message: None, + cause: Some(Left(err.to_string())), + status: SomethingWentWrong + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct KeyValue { + context_value: Value, + dimension_to_be_dropped: String, +} + +#[post("")] +pub async fn promote_contexts_overrides(input_state: Data, body: Json) -> Result, AppError> { + let state = &input_state; + + let input_context = body.context_value.clone(); + let dimension_to_be_dropped = body.dimension_to_be_dropped.clone(); + + let get_processed_context_and_hash_values = |keys_to_be_excluded: Option<&Vec>| -> Result<(String, Value), AppError>{ + let (processed_context_value, _) = + process_input_context_json(&input_context, keys_to_be_excluded)?; + let context_value = + to_value(processed_context_value).map_err(default_parsing_error)?; + let hashed_value = + string_based_b64_hash(context_value.to_string()).to_string(); + + Ok((hashed_value, context_value)) + }; + + let (existing_hashed_value, _) = get_processed_context_and_hash_values(None)?; + let (_, new_context_value) = get_processed_context_and_hash_values(Some(&Vec::from([dimension_to_be_dropped])))?; + + // Add transaction wrapper here + let override_id = fetch_override_from_ctx_id(&state, &existing_hashed_value).await?; + let context_response = add_new_context_v2(&state, new_context_value, true).await?; + add_ctx_override(&state, context_response.id, override_id, true).await + +} \ No newline at end of file diff --git a/backend/src/api/derived/reduce.rs b/backend/src/api/derived/reduce.rs index 26411b84f..8ea776b92 100644 --- a/backend/src/api/derived/reduce.rs +++ b/backend/src/api/derived/reduce.rs @@ -15,7 +15,7 @@ use crate::api::primary::{ }; -use crate::{AppState}; +use crate::AppState; use std::iter::FromIterator; diff --git a/backend/src/api/primary/new_contexts.rs b/backend/src/api/primary/new_contexts.rs index 899cf9243..d5fec7ca1 100644 --- a/backend/src/api/primary/new_contexts.rs +++ b/backend/src/api/primary/new_contexts.rs @@ -60,6 +60,8 @@ fn get_dimension_name (idx: usize) -> String { dimesions[idx].to_string() } +type ContextValueAndDimensionMap = (HashMap, HashMap); + fn contexts_comparator(priority_map: HashMap) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { let get_value_from_map = move |key: Option| { @@ -150,9 +152,10 @@ pub async fn fetch_raw_context_v2( } -pub fn process_single_condition(value_object: &HashMap) -> HashMap { +pub fn process_single_condition(value_object: &HashMap, keys_to_be_excluded: Option<&Vec>) -> ContextValueAndDimensionMap { - let mut result_map: HashMap = HashMap::new(); + let mut result_map: HashMap = HashMap::new(); + let mut result_context: HashMap = HashMap::new(); // Range check have to be implemented if value_object.contains_key("==") { @@ -165,44 +168,60 @@ pub fn process_single_condition(value_object: &HashMap) -> HashMa if let Some(key) = variable_map.get("var") { let key_string = key.to_string(); - - result_map.insert( - // Removing double quotes at the start and end for both key and value - strip_double_quotes(&key_string).to_owned(), - strip_double_quotes(&mapped_value).to_owned() - ); + let to_be_included = + keys_to_be_excluded.map_or( + true, + |vect| !vect.contains(&key_string) + ); + + + if to_be_included { + result_map.insert( + // Removing double quotes at the start and end for both key and value + strip_double_quotes(&key_string).to_owned(), + strip_double_quotes(&mapped_value).to_owned() + ); + + result_context = value_object.to_owned(); + } } } } - result_map + (result_context, result_map) } -pub fn process_input_context_json(input_json: &Value) -> HashMap { +pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Option<&Vec>) -> Result { - let input_as_map: HashMap = from_value(input_json.to_owned()).unwrap(); // map_err(default_parsing_error)?; + let input_as_map: HashMap = from_value(input_json.to_owned()).map_err(default_parsing_error)?; + let mut result_context: HashMap = HashMap::new(); + // Single dimension or condition if !input_as_map.contains_key("and") { - return process_single_condition(&input_as_map); + return Ok(process_single_condition(&input_as_map, keys_to_be_excluded)); } - let mut result_map: HashMap = HashMap::new(); + let mut result_map = HashMap::new(); let val = input_as_map.get("and").unwrap(); let multiple_condition_array: Vec = from_value(val.to_owned()).unwrap(); for item in multiple_condition_array { - let value_object: HashMap = from_value(item).unwrap(); // .map_err(default_parsing_error)?; - result_map.extend(process_single_condition(&value_object)); + let value_object: HashMap = from_value(item).map_err(default_parsing_error)?; + let (ctx, map) = process_single_condition(&value_object, keys_to_be_excluded); + result_context.extend(ctx); + result_map.extend(map); } - result_map + Ok((result_context, result_map)) } -pub async fn add_new_context_v2(state: &Data, context_value: Value, return_if_present: bool) -> Result { +pub async fn add_new_context_v2(state: &Data, raw_context_value: Value, return_if_present: bool) -> Result { let db: Addr = state.db.clone(); - let key_value_map = process_input_context_json(&context_value); + let (processed_context_value, key_value_map) = + process_input_context_json(&raw_context_value, None)?; + let context_value = to_value(processed_context_value).map_err(default_parsing_error)?; // ? TODO :: Post as an array of value // ? TODO :: Sort query based on key and add to DB diff --git a/backend/src/main.rs b/backend/src/main.rs index 767e6b7d8..4b2b9090a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -43,6 +43,7 @@ use api::derived::{ config::get_config, context_override::add_new_context_override, reduce::reduce_contexts_overrides, + promote::promote_contexts_overrides, }; // use crate::utils::validations::just_for_test; @@ -123,6 +124,10 @@ async fn main() -> Result<()> { scope("reduce") .service(reduce_contexts_overrides) ) + .service( + scope("promote") + .service(promote_contexts_overrides) + ) }) .bind(("localhost", 8080))? .workers(5) From 48fd6386bfae1c8d98d9c45da50b4e9853f7ae9d Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Wed, 8 Feb 2023 12:51:14 +0530 Subject: [PATCH 006/352] Promote fixes --- backend/src/api/derived/promote.rs | 7 +++-- backend/src/api/primary/context_overrides.rs | 10 ++++-- backend/src/api/primary/new_contexts.rs | 33 +++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/backend/src/api/derived/promote.rs b/backend/src/api/derived/promote.rs index 182e2f338..724996e46 100644 --- a/backend/src/api/derived/promote.rs +++ b/backend/src/api/derived/promote.rs @@ -8,8 +8,8 @@ use serde_json::{to_value, Error, Value}; use serde::{Serialize, Deserialize}; use crate::api::primary::{ - context_overrides::{ContextOverrideResponse, add_ctx_override, fetch_override_from_ctx_id}, - new_contexts::{add_new_context_v2, process_input_context_json} + context_overrides::{ContextOverrideResponse, add_ctx_override, delete_ctx_override_helper, fetch_override_from_ctx_id}, + new_contexts::{add_new_context_v2, delete_new_context_helper, process_input_context_json} }; use crate::AppState; use crate::utils::{ @@ -60,6 +60,9 @@ pub async fn promote_contexts_overrides(input_state: Data, body: Json< // Add transaction wrapper here let override_id = fetch_override_from_ctx_id(&state, &existing_hashed_value).await?; let context_response = add_new_context_v2(&state, new_context_value, true).await?; + + delete_new_context_helper(&state, existing_hashed_value.clone()).await?; + delete_ctx_override_helper(&state, existing_hashed_value).await?; add_ctx_override(&state, context_response.id, override_id, true).await } \ No newline at end of file diff --git a/backend/src/api/primary/context_overrides.rs b/backend/src/api/primary/context_overrides.rs index f075d6aee..64564c2f8 100644 --- a/backend/src/api/primary/context_overrides.rs +++ b/backend/src/api/primary/context_overrides.rs @@ -124,9 +124,8 @@ pub async fn get_ctx_override(state: Data, id: Path) -> Result )) } -#[delete("/{id}")] -pub async fn delete_ctx_override(state: Data, id: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); +pub async fn delete_ctx_override_helper(state: &Data, id: String) -> Result, AppError> { + let db: Addr = state.db.clone(); match db .send(DeleteCtxOverrides { @@ -143,3 +142,8 @@ pub async fn delete_ctx_override(state: Data, id: Path) -> Res Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } + +#[delete("/{id}")] +pub async fn delete_ctx_override(state: Data, id: Path) -> Result, AppError> { + delete_ctx_override_helper(&state, id.to_string()).await +} diff --git a/backend/src/api/primary/new_contexts.rs b/backend/src/api/primary/new_contexts.rs index d5fec7ca1..708f13532 100644 --- a/backend/src/api/primary/new_contexts.rs +++ b/backend/src/api/primary/new_contexts.rs @@ -61,6 +61,7 @@ fn get_dimension_name (idx: usize) -> String { } type ContextValueAndDimensionMap = (HashMap, HashMap); +type MaybeContextValueAndDimensionMap = (Option>, HashMap); fn contexts_comparator(priority_map: HashMap) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { @@ -152,10 +153,10 @@ pub async fn fetch_raw_context_v2( } -pub fn process_single_condition(value_object: &HashMap, keys_to_be_excluded: Option<&Vec>) -> ContextValueAndDimensionMap { +pub fn process_single_condition(value_object: &HashMap, keys_to_be_excluded: Option<&Vec>) -> MaybeContextValueAndDimensionMap { let mut result_map: HashMap = HashMap::new(); - let mut result_context: HashMap = HashMap::new(); + let mut result_context: Option> = None; // Range check have to be implemented if value_object.contains_key("==") { @@ -167,14 +168,13 @@ pub fn process_single_condition(value_object: &HashMap, keys_to_b let mapped_value = variable_array[1].to_string(); if let Some(key) = variable_map.get("var") { - let key_string = key.to_string(); + let key_string = strip_double_quotes(&key.to_string()).to_string(); let to_be_included = keys_to_be_excluded.map_or( true, |vect| !vect.contains(&key_string) ); - if to_be_included { result_map.insert( // Removing double quotes at the start and end for both key and value @@ -182,7 +182,7 @@ pub fn process_single_condition(value_object: &HashMap, keys_to_b strip_double_quotes(&mapped_value).to_owned() ); - result_context = value_object.to_owned(); + result_context = Some(value_object.to_owned()); } } } @@ -194,11 +194,17 @@ pub fn process_single_condition(value_object: &HashMap, keys_to_b pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Option<&Vec>) -> Result { let input_as_map: HashMap = from_value(input_json.to_owned()).map_err(default_parsing_error)?; - let mut result_context: HashMap = HashMap::new(); + let mut result_context_array = Vec::new(); // Single dimension or condition if !input_as_map.contains_key("and") { - return Ok(process_single_condition(&input_as_map, keys_to_be_excluded)); + // ! TODO :: Fix this asap + return Err(AppError { + message: None, + cause: Some(Left("Case yet to handled properly".to_string())), + status: SomethingWentWrong + }); + // return Ok(process_single_condition(&input_as_map, keys_to_be_excluded)); } let mut result_map = HashMap::new(); @@ -208,12 +214,17 @@ pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Optio for item in multiple_condition_array { let value_object: HashMap = from_value(item).map_err(default_parsing_error)?; - let (ctx, map) = process_single_condition(&value_object, keys_to_be_excluded); - result_context.extend(ctx); - result_map.extend(map); + let (maybe_ctx, map) = process_single_condition(&value_object, keys_to_be_excluded); + + if let Some(ctx) = maybe_ctx { + result_context_array.push(ctx); + result_map.extend(map); + } } - Ok((result_context, result_map)) + Ok((HashMap::from([ + ("and".to_string(), to_value(result_context_array).map_err(default_parsing_error)?) + ]), result_map)) } pub async fn add_new_context_v2(state: &Data, raw_context_value: Value, return_if_present: bool) -> Result { From ec446d2dfc013e4a666af33852473b83f205b9a2 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 10 May 2023 19:57:49 +0530 Subject: [PATCH 007/352] flakes first cut --- .gitignore | 11 +- backend/.env | 1 + backend/Cargo.lock | 1777 ++++++++++++++++++++++++++++++++++++++ backend/flake.lock | 94 ++ backend/flake.nix | 38 + backend/src/db/schema.rs | 38 +- 6 files changed, 1934 insertions(+), 25 deletions(-) create mode 100644 backend/.env create mode 100644 backend/Cargo.lock create mode 100644 backend/flake.lock create mode 100644 backend/flake.nix diff --git a/.gitignore b/.gitignore index d84691250..5ea867ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,17 +8,11 @@ node_modules/ dist/ build/ - - /backend/target # Compiled Java class files *.class - - -*Cargo.lock - # Compiled Python bytecode *.py[cod] @@ -59,3 +53,8 @@ Thumbs.db .package-lock.json +# nix build outputs +backend/result +backend/.tmp +backend/logfile +backend/*PGSQL* diff --git a/backend/.env b/backend/.env new file mode 100644 index 000000000..399b1e611 --- /dev/null +++ b/backend/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://$USER:postgres@localhost/context_aware_config diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 000000000..376303ce1 --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,1777 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.21", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "context-aware-config" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "derive_more", + "diesel", + "dotenv", + "env_logger", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "uuid 0.8.2", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.21", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.15", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diesel" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", + "serde_json", + "uuid 1.3.2", +] + +[[package]] +name = "diesel_derives" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad74fdcf086be3d4fdd142f67937678fe60ed431c3b2f08599e7687269410c4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "uuid" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.15", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/backend/flake.lock b/backend/flake.lock new file mode 100644 index 000000000..3600e86de --- /dev/null +++ b/backend/flake.lock @@ -0,0 +1,94 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1679567394, + "narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=", + "owner": "nix-community", + "repo": "naersk", + "rev": "88cd22380154a2c36799fe8098888f0f59861a15", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1683657389, + "narHash": "sha256-jx91UqqoBneE8QPAKJA29GANrU/Z7ULghoa/JE0+Edw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9524f57dd5b3944c819dd594aed8ed941932ef56", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1683657389, + "narHash": "sha256-jx91UqqoBneE8QPAKJA29GANrU/Z7ULghoa/JE0+Edw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9524f57dd5b3944c819dd594aed8ed941932ef56", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/backend/flake.nix b/backend/flake.nix new file mode 100644 index 000000000..626e9a3aa --- /dev/null +++ b/backend/flake.nix @@ -0,0 +1,38 @@ +{ + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + naersk.url = "github:nix-community/naersk"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = { self, flake-utils, naersk, nixpkgs }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = (import nixpkgs) { + inherit system; + }; + + naersk' = pkgs.callPackage naersk {}; + + in rec { + # For `nix build` & `nix run`: + defaultPackage = naersk'.buildPackage { + nativeBuildInputs = with pkgs; [ postgresql_12 ]; + src = ./.; + }; + + # For `nix develop`: + devShell = pkgs.mkShell { + nativeBuildInputs = with pkgs; + [ + rustc + cargo + postgresql_12 + libiconv + openssl + ]; + # buildInputs = with pkgs; [ ]; + }; + } + ); +} diff --git a/backend/src/db/schema.rs b/backend/src/db/schema.rs index 8fd4da13d..964a8e185 100644 --- a/backend/src/db/schema.rs +++ b/backend/src/db/schema.rs @@ -1,25 +1,34 @@ // @generated automatically by Diesel CLI. diesel::table! { - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, + contexts (key) { + key -> Varchar, + value -> Varchar, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - global_config (key) { - key -> Varchar, - value -> Json, + ctxoverrides (context_id) { + context_id -> Varchar, + override_id -> Varchar, last_modified -> Timestamptz, created_on -> Timestamptz, } } diesel::table! { - overrides (key) { + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + last_modified -> Timestamptz, + created_on -> Timestamptz, + } +} + +diesel::table! { + global_config (key) { key -> Varchar, value -> Json, last_modified -> Timestamptz, @@ -28,9 +37,9 @@ diesel::table! { } diesel::table! { - contexts (key) { + overrides (key) { key -> Varchar, - value -> Varchar, + value -> Json, last_modified -> Timestamptz, created_on -> Timestamptz, } @@ -49,20 +58,11 @@ diesel::table! { } } -diesel::table! { - ctxoverrides (context_id) { - context_id -> Varchar, - override_id -> Varchar, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - diesel::allow_tables_to_appear_in_same_query!( contexts, ctxoverrides, dimensions, global_config, overrides, - newcontexts + newcontexts, ); From 155efe5f9f72fdeae6803dc516d0efe1b4828ca6 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 15 May 2023 12:00:17 +0530 Subject: [PATCH 008/352] added create query for newcontexts table --- backend/scripts/tables.sql | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/scripts/tables.sql b/backend/scripts/tables.sql index e91923470..8add3e8b9 100644 --- a/backend/scripts/tables.sql +++ b/backend/scripts/tables.sql @@ -37,4 +37,16 @@ CREATE TABLE ctxoverrides ( last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY(context_id) -); \ No newline at end of file +); + +CREATE TABLE newcontexts ( + key VARCHAR NOT NULL, + value JSON NOT NULL, + column1 VARCHAR, + column2 VARCHAR, + column3 VARCHAR, + column4 VARCHAR, + last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY(key) +); From 89cf0ec3e4e4090841c5a74f52eff5ab000b973f Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 16 May 2023 17:55:58 +0530 Subject: [PATCH 009/352] ci: made some miscellaneous changes for local setup * also figured out the database url which should be used to access postgres outside of docker * enabled auto db_init --- .env | 1 - .gitignore | 3 ++- backend/.env | 1 - Dockerfile => backend/Dockerfile | 4 ++-- docker-compose.yaml => backend/docker-compose.yaml | 6 ++++-- backend/docker-compose/postgres/Dockerfile | 3 +++ .../tables.sql => docker-compose/postgres/db_init.sql} | 0 backend/flake.nix | 3 +++ makefile => backend/makefile | 0 9 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 .env delete mode 100644 backend/.env rename Dockerfile => backend/Dockerfile (94%) rename docker-compose.yaml => backend/docker-compose.yaml (75%) create mode 100644 backend/docker-compose/postgres/Dockerfile rename backend/{scripts/tables.sql => docker-compose/postgres/db_init.sql} (100%) rename makefile => backend/makefile (100%) diff --git a/.env b/.env deleted file mode 100644 index f69dce4b9..000000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=postgres://utkarsh.pandey:postgres@localhost/config diff --git a/.gitignore b/.gitignore index 5ea867ed8..c4dbc91e3 100644 --- a/.gitignore +++ b/.gitignore @@ -51,10 +51,11 @@ Thumbs.db *.mov *.wmv -.package-lock.json +.env # nix build outputs backend/result backend/.tmp backend/logfile backend/*PGSQL* +backend/.env diff --git a/backend/.env b/backend/.env deleted file mode 100644 index 399b1e611..000000000 --- a/backend/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=postgres://$USER:postgres@localhost/context_aware_config diff --git a/Dockerfile b/backend/Dockerfile similarity index 94% rename from Dockerfile rename to backend/Dockerfile index 727bdc660..2dccde302 100644 --- a/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,7 @@ FROM rust as planner WORKDIR /app RUN cargo install cargo-chef -COPY ./backend . +COPY . . RUN cargo chef prepare --recipe-path recipe.json FROM rust as cacher @@ -11,7 +11,7 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json FROM rust as builder -COPY ./backend /app +COPY . /app WORKDIR /app COPY --from=cacher /app/target target COPY --from=cacher /usr/local/cargo /usr/local/cargo diff --git a/docker-compose.yaml b/backend/docker-compose.yaml similarity index 75% rename from docker-compose.yaml rename to backend/docker-compose.yaml index 3f210a65f..b4ff67276 100644 --- a/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -5,12 +5,13 @@ services: command: ./context-aware-config build: dockerfile: Dockerfile - context: ./backend + context: . links: - postgres depends_on: - postgres environment: + # to access postgres outside of docker, use postgres://postgres:docker@localhost:5432 instead - DATABASE_URL=postgres://postgres:docker@postgres:5432/config?sslmode=disable - DB_POOL_SIZE=10 @@ -22,7 +23,8 @@ services: # Middleware postgres: - image: postgres:12 + build: ./docker-compose/postgres/ + container_name: context-aware-config_postgres_instance environment: POSTGRES_PASSWORD: "docker" POSTGRES_DB: "config" diff --git a/backend/docker-compose/postgres/Dockerfile b/backend/docker-compose/postgres/Dockerfile new file mode 100644 index 000000000..a6707ff39 --- /dev/null +++ b/backend/docker-compose/postgres/Dockerfile @@ -0,0 +1,3 @@ +FROM postgres:12 + +COPY ./db_init.sql /docker-entrypoint-initdb.d/db_init.sql diff --git a/backend/scripts/tables.sql b/backend/docker-compose/postgres/db_init.sql similarity index 100% rename from backend/scripts/tables.sql rename to backend/docker-compose/postgres/db_init.sql diff --git a/backend/flake.nix b/backend/flake.nix index 626e9a3aa..f08f13f91 100644 --- a/backend/flake.nix +++ b/backend/flake.nix @@ -30,6 +30,9 @@ postgresql_12 libiconv openssl + docker-compose + rust-analyzer + rustfmt ]; # buildInputs = with pkgs; [ ]; }; diff --git a/makefile b/backend/makefile similarity index 100% rename from makefile rename to backend/makefile From a09201d89e692aa317ce3361fd668327e5e15351 Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Wed, 17 May 2023 15:16:16 +0530 Subject: [PATCH 010/352] Refactored directory structure. --- .envrc | 1 + .gitignore | 1 + backend/Cargo.lock => Cargo.lock | 0 backend/Cargo.toml => Cargo.toml | 0 backend/Dockerfile => Dockerfile | 0 backend/diesel.toml => diesel.toml | 0 backend/docker-compose.yaml => docker-compose.yaml | 0 {backend/docker-compose => docker-compose}/postgres/Dockerfile | 0 {backend/docker-compose => docker-compose}/postgres/db_init.sql | 0 backend/flake.lock => flake.lock | 0 backend/flake.nix => flake.nix | 0 {src => libs/js}/index.ts | 0 package-lock.json => libs/js/package-lock.json | 0 package.json => libs/js/package.json | 0 tsconfig.json => libs/js/tsconfig.json | 0 {src => libs/js}/types.ts | 0 {src => libs/js}/utils/deepMerge.ts | 0 {src => libs/js}/utils/operations.ts | 0 backend/makefile => makefile | 0 {backend/migrations => migrations}/.keep | 0 .../00000000000000_diesel_initial_setup/down.sql | 0 .../00000000000000_diesel_initial_setup/up.sql | 0 {backend/scripts => scripts}/server-setup.sh | 0 {backend/src => src}/api/derived/config.rs | 0 {backend/src => src}/api/derived/context_override.rs | 0 {backend/src => src}/api/derived/mod.rs | 0 {backend/src => src}/api/derived/promote.rs | 0 {backend/src => src}/api/derived/reduce.rs | 0 {backend/src => src}/api/mod.rs | 0 {backend/src => src}/api/primary/context_overrides.rs | 0 {backend/src => src}/api/primary/contexts.rs | 0 {backend/src => src}/api/primary/dimensions.rs | 0 {backend/src => src}/api/primary/global_config.rs | 0 {backend/src => src}/api/primary/mod.rs | 0 {backend/src => src}/api/primary/new_contexts.rs | 0 {backend/src => src}/api/primary/overrides.rs | 0 {backend/src => src}/db/mod.rs | 0 {backend/src => src}/db/schema.rs | 0 {backend/src => src}/db/utils.rs | 0 {backend/src => src}/handlers/context_overrides.rs | 0 {backend/src => src}/handlers/contexts.rs | 0 {backend/src => src}/handlers/dimensions.rs | 0 {backend/src => src}/handlers/global_config.rs | 0 {backend/src => src}/handlers/mod.rs | 0 {backend/src => src}/handlers/new_contexts.rs | 0 {backend/src => src}/handlers/overrides.rs | 0 {backend/src => src}/main.rs | 0 {backend/src => src}/messages/context_overrides.rs | 0 {backend/src => src}/messages/contexts.rs | 0 {backend/src => src}/messages/dimensions.rs | 0 {backend/src => src}/messages/global_config.rs | 0 {backend/src => src}/messages/mod.rs | 0 {backend/src => src}/messages/new_contexts.rs | 0 {backend/src => src}/messages/overrides.rs | 0 {backend/src => src}/models/db_models.rs | 0 {backend/src => src}/models/insertables/context_overrides.rs | 0 {backend/src => src}/models/insertables/contexts.rs | 0 {backend/src => src}/models/insertables/dimensions.rs | 0 {backend/src => src}/models/insertables/global_config.rs | 0 {backend/src => src}/models/insertables/mod.rs | 0 {backend/src => src}/models/insertables/new_contexts.rs | 0 {backend/src => src}/models/insertables/overrides.rs | 0 {backend/src => src}/models/mod.rs | 0 {backend/src => src}/utils/errors.rs | 0 {backend/src => src}/utils/hash.rs | 0 {backend/src => src}/utils/helpers.rs | 0 {backend/src => src}/utils/mod.rs | 0 {backend/src => src}/utils/validations.rs | 0 68 files changed, 2 insertions(+) create mode 100644 .envrc rename backend/Cargo.lock => Cargo.lock (100%) rename backend/Cargo.toml => Cargo.toml (100%) rename backend/Dockerfile => Dockerfile (100%) rename backend/diesel.toml => diesel.toml (100%) rename backend/docker-compose.yaml => docker-compose.yaml (100%) rename {backend/docker-compose => docker-compose}/postgres/Dockerfile (100%) rename {backend/docker-compose => docker-compose}/postgres/db_init.sql (100%) rename backend/flake.lock => flake.lock (100%) rename backend/flake.nix => flake.nix (100%) rename {src => libs/js}/index.ts (100%) rename package-lock.json => libs/js/package-lock.json (100%) rename package.json => libs/js/package.json (100%) rename tsconfig.json => libs/js/tsconfig.json (100%) rename {src => libs/js}/types.ts (100%) rename {src => libs/js}/utils/deepMerge.ts (100%) rename {src => libs/js}/utils/operations.ts (100%) rename backend/makefile => makefile (100%) rename {backend/migrations => migrations}/.keep (100%) rename {backend/migrations => migrations}/00000000000000_diesel_initial_setup/down.sql (100%) rename {backend/migrations => migrations}/00000000000000_diesel_initial_setup/up.sql (100%) rename {backend/scripts => scripts}/server-setup.sh (100%) rename {backend/src => src}/api/derived/config.rs (100%) rename {backend/src => src}/api/derived/context_override.rs (100%) rename {backend/src => src}/api/derived/mod.rs (100%) rename {backend/src => src}/api/derived/promote.rs (100%) rename {backend/src => src}/api/derived/reduce.rs (100%) rename {backend/src => src}/api/mod.rs (100%) rename {backend/src => src}/api/primary/context_overrides.rs (100%) rename {backend/src => src}/api/primary/contexts.rs (100%) rename {backend/src => src}/api/primary/dimensions.rs (100%) rename {backend/src => src}/api/primary/global_config.rs (100%) rename {backend/src => src}/api/primary/mod.rs (100%) rename {backend/src => src}/api/primary/new_contexts.rs (100%) rename {backend/src => src}/api/primary/overrides.rs (100%) rename {backend/src => src}/db/mod.rs (100%) rename {backend/src => src}/db/schema.rs (100%) rename {backend/src => src}/db/utils.rs (100%) rename {backend/src => src}/handlers/context_overrides.rs (100%) rename {backend/src => src}/handlers/contexts.rs (100%) rename {backend/src => src}/handlers/dimensions.rs (100%) rename {backend/src => src}/handlers/global_config.rs (100%) rename {backend/src => src}/handlers/mod.rs (100%) rename {backend/src => src}/handlers/new_contexts.rs (100%) rename {backend/src => src}/handlers/overrides.rs (100%) rename {backend/src => src}/main.rs (100%) rename {backend/src => src}/messages/context_overrides.rs (100%) rename {backend/src => src}/messages/contexts.rs (100%) rename {backend/src => src}/messages/dimensions.rs (100%) rename {backend/src => src}/messages/global_config.rs (100%) rename {backend/src => src}/messages/mod.rs (100%) rename {backend/src => src}/messages/new_contexts.rs (100%) rename {backend/src => src}/messages/overrides.rs (100%) rename {backend/src => src}/models/db_models.rs (100%) rename {backend/src => src}/models/insertables/context_overrides.rs (100%) rename {backend/src => src}/models/insertables/contexts.rs (100%) rename {backend/src => src}/models/insertables/dimensions.rs (100%) rename {backend/src => src}/models/insertables/global_config.rs (100%) rename {backend/src => src}/models/insertables/mod.rs (100%) rename {backend/src => src}/models/insertables/new_contexts.rs (100%) rename {backend/src => src}/models/insertables/overrides.rs (100%) rename {backend/src => src}/models/mod.rs (100%) rename {backend/src => src}/utils/errors.rs (100%) rename {backend/src => src}/utils/hash.rs (100%) rename {backend/src => src}/utils/helpers.rs (100%) rename {backend/src => src}/utils/mod.rs (100%) rename {backend/src => src}/utils/validations.rs (100%) diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index c4dbc91e3..27cfebc72 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ backend/.tmp backend/logfile backend/*PGSQL* backend/.env +.direnv \ No newline at end of file diff --git a/backend/Cargo.lock b/Cargo.lock similarity index 100% rename from backend/Cargo.lock rename to Cargo.lock diff --git a/backend/Cargo.toml b/Cargo.toml similarity index 100% rename from backend/Cargo.toml rename to Cargo.toml diff --git a/backend/Dockerfile b/Dockerfile similarity index 100% rename from backend/Dockerfile rename to Dockerfile diff --git a/backend/diesel.toml b/diesel.toml similarity index 100% rename from backend/diesel.toml rename to diesel.toml diff --git a/backend/docker-compose.yaml b/docker-compose.yaml similarity index 100% rename from backend/docker-compose.yaml rename to docker-compose.yaml diff --git a/backend/docker-compose/postgres/Dockerfile b/docker-compose/postgres/Dockerfile similarity index 100% rename from backend/docker-compose/postgres/Dockerfile rename to docker-compose/postgres/Dockerfile diff --git a/backend/docker-compose/postgres/db_init.sql b/docker-compose/postgres/db_init.sql similarity index 100% rename from backend/docker-compose/postgres/db_init.sql rename to docker-compose/postgres/db_init.sql diff --git a/backend/flake.lock b/flake.lock similarity index 100% rename from backend/flake.lock rename to flake.lock diff --git a/backend/flake.nix b/flake.nix similarity index 100% rename from backend/flake.nix rename to flake.nix diff --git a/src/index.ts b/libs/js/index.ts similarity index 100% rename from src/index.ts rename to libs/js/index.ts diff --git a/package-lock.json b/libs/js/package-lock.json similarity index 100% rename from package-lock.json rename to libs/js/package-lock.json diff --git a/package.json b/libs/js/package.json similarity index 100% rename from package.json rename to libs/js/package.json diff --git a/tsconfig.json b/libs/js/tsconfig.json similarity index 100% rename from tsconfig.json rename to libs/js/tsconfig.json diff --git a/src/types.ts b/libs/js/types.ts similarity index 100% rename from src/types.ts rename to libs/js/types.ts diff --git a/src/utils/deepMerge.ts b/libs/js/utils/deepMerge.ts similarity index 100% rename from src/utils/deepMerge.ts rename to libs/js/utils/deepMerge.ts diff --git a/src/utils/operations.ts b/libs/js/utils/operations.ts similarity index 100% rename from src/utils/operations.ts rename to libs/js/utils/operations.ts diff --git a/backend/makefile b/makefile similarity index 100% rename from backend/makefile rename to makefile diff --git a/backend/migrations/.keep b/migrations/.keep similarity index 100% rename from backend/migrations/.keep rename to migrations/.keep diff --git a/backend/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from backend/migrations/00000000000000_diesel_initial_setup/down.sql rename to migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/backend/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from backend/migrations/00000000000000_diesel_initial_setup/up.sql rename to migrations/00000000000000_diesel_initial_setup/up.sql diff --git a/backend/scripts/server-setup.sh b/scripts/server-setup.sh similarity index 100% rename from backend/scripts/server-setup.sh rename to scripts/server-setup.sh diff --git a/backend/src/api/derived/config.rs b/src/api/derived/config.rs similarity index 100% rename from backend/src/api/derived/config.rs rename to src/api/derived/config.rs diff --git a/backend/src/api/derived/context_override.rs b/src/api/derived/context_override.rs similarity index 100% rename from backend/src/api/derived/context_override.rs rename to src/api/derived/context_override.rs diff --git a/backend/src/api/derived/mod.rs b/src/api/derived/mod.rs similarity index 100% rename from backend/src/api/derived/mod.rs rename to src/api/derived/mod.rs diff --git a/backend/src/api/derived/promote.rs b/src/api/derived/promote.rs similarity index 100% rename from backend/src/api/derived/promote.rs rename to src/api/derived/promote.rs diff --git a/backend/src/api/derived/reduce.rs b/src/api/derived/reduce.rs similarity index 100% rename from backend/src/api/derived/reduce.rs rename to src/api/derived/reduce.rs diff --git a/backend/src/api/mod.rs b/src/api/mod.rs similarity index 100% rename from backend/src/api/mod.rs rename to src/api/mod.rs diff --git a/backend/src/api/primary/context_overrides.rs b/src/api/primary/context_overrides.rs similarity index 100% rename from backend/src/api/primary/context_overrides.rs rename to src/api/primary/context_overrides.rs diff --git a/backend/src/api/primary/contexts.rs b/src/api/primary/contexts.rs similarity index 100% rename from backend/src/api/primary/contexts.rs rename to src/api/primary/contexts.rs diff --git a/backend/src/api/primary/dimensions.rs b/src/api/primary/dimensions.rs similarity index 100% rename from backend/src/api/primary/dimensions.rs rename to src/api/primary/dimensions.rs diff --git a/backend/src/api/primary/global_config.rs b/src/api/primary/global_config.rs similarity index 100% rename from backend/src/api/primary/global_config.rs rename to src/api/primary/global_config.rs diff --git a/backend/src/api/primary/mod.rs b/src/api/primary/mod.rs similarity index 100% rename from backend/src/api/primary/mod.rs rename to src/api/primary/mod.rs diff --git a/backend/src/api/primary/new_contexts.rs b/src/api/primary/new_contexts.rs similarity index 100% rename from backend/src/api/primary/new_contexts.rs rename to src/api/primary/new_contexts.rs diff --git a/backend/src/api/primary/overrides.rs b/src/api/primary/overrides.rs similarity index 100% rename from backend/src/api/primary/overrides.rs rename to src/api/primary/overrides.rs diff --git a/backend/src/db/mod.rs b/src/db/mod.rs similarity index 100% rename from backend/src/db/mod.rs rename to src/db/mod.rs diff --git a/backend/src/db/schema.rs b/src/db/schema.rs similarity index 100% rename from backend/src/db/schema.rs rename to src/db/schema.rs diff --git a/backend/src/db/utils.rs b/src/db/utils.rs similarity index 100% rename from backend/src/db/utils.rs rename to src/db/utils.rs diff --git a/backend/src/handlers/context_overrides.rs b/src/handlers/context_overrides.rs similarity index 100% rename from backend/src/handlers/context_overrides.rs rename to src/handlers/context_overrides.rs diff --git a/backend/src/handlers/contexts.rs b/src/handlers/contexts.rs similarity index 100% rename from backend/src/handlers/contexts.rs rename to src/handlers/contexts.rs diff --git a/backend/src/handlers/dimensions.rs b/src/handlers/dimensions.rs similarity index 100% rename from backend/src/handlers/dimensions.rs rename to src/handlers/dimensions.rs diff --git a/backend/src/handlers/global_config.rs b/src/handlers/global_config.rs similarity index 100% rename from backend/src/handlers/global_config.rs rename to src/handlers/global_config.rs diff --git a/backend/src/handlers/mod.rs b/src/handlers/mod.rs similarity index 100% rename from backend/src/handlers/mod.rs rename to src/handlers/mod.rs diff --git a/backend/src/handlers/new_contexts.rs b/src/handlers/new_contexts.rs similarity index 100% rename from backend/src/handlers/new_contexts.rs rename to src/handlers/new_contexts.rs diff --git a/backend/src/handlers/overrides.rs b/src/handlers/overrides.rs similarity index 100% rename from backend/src/handlers/overrides.rs rename to src/handlers/overrides.rs diff --git a/backend/src/main.rs b/src/main.rs similarity index 100% rename from backend/src/main.rs rename to src/main.rs diff --git a/backend/src/messages/context_overrides.rs b/src/messages/context_overrides.rs similarity index 100% rename from backend/src/messages/context_overrides.rs rename to src/messages/context_overrides.rs diff --git a/backend/src/messages/contexts.rs b/src/messages/contexts.rs similarity index 100% rename from backend/src/messages/contexts.rs rename to src/messages/contexts.rs diff --git a/backend/src/messages/dimensions.rs b/src/messages/dimensions.rs similarity index 100% rename from backend/src/messages/dimensions.rs rename to src/messages/dimensions.rs diff --git a/backend/src/messages/global_config.rs b/src/messages/global_config.rs similarity index 100% rename from backend/src/messages/global_config.rs rename to src/messages/global_config.rs diff --git a/backend/src/messages/mod.rs b/src/messages/mod.rs similarity index 100% rename from backend/src/messages/mod.rs rename to src/messages/mod.rs diff --git a/backend/src/messages/new_contexts.rs b/src/messages/new_contexts.rs similarity index 100% rename from backend/src/messages/new_contexts.rs rename to src/messages/new_contexts.rs diff --git a/backend/src/messages/overrides.rs b/src/messages/overrides.rs similarity index 100% rename from backend/src/messages/overrides.rs rename to src/messages/overrides.rs diff --git a/backend/src/models/db_models.rs b/src/models/db_models.rs similarity index 100% rename from backend/src/models/db_models.rs rename to src/models/db_models.rs diff --git a/backend/src/models/insertables/context_overrides.rs b/src/models/insertables/context_overrides.rs similarity index 100% rename from backend/src/models/insertables/context_overrides.rs rename to src/models/insertables/context_overrides.rs diff --git a/backend/src/models/insertables/contexts.rs b/src/models/insertables/contexts.rs similarity index 100% rename from backend/src/models/insertables/contexts.rs rename to src/models/insertables/contexts.rs diff --git a/backend/src/models/insertables/dimensions.rs b/src/models/insertables/dimensions.rs similarity index 100% rename from backend/src/models/insertables/dimensions.rs rename to src/models/insertables/dimensions.rs diff --git a/backend/src/models/insertables/global_config.rs b/src/models/insertables/global_config.rs similarity index 100% rename from backend/src/models/insertables/global_config.rs rename to src/models/insertables/global_config.rs diff --git a/backend/src/models/insertables/mod.rs b/src/models/insertables/mod.rs similarity index 100% rename from backend/src/models/insertables/mod.rs rename to src/models/insertables/mod.rs diff --git a/backend/src/models/insertables/new_contexts.rs b/src/models/insertables/new_contexts.rs similarity index 100% rename from backend/src/models/insertables/new_contexts.rs rename to src/models/insertables/new_contexts.rs diff --git a/backend/src/models/insertables/overrides.rs b/src/models/insertables/overrides.rs similarity index 100% rename from backend/src/models/insertables/overrides.rs rename to src/models/insertables/overrides.rs diff --git a/backend/src/models/mod.rs b/src/models/mod.rs similarity index 100% rename from backend/src/models/mod.rs rename to src/models/mod.rs diff --git a/backend/src/utils/errors.rs b/src/utils/errors.rs similarity index 100% rename from backend/src/utils/errors.rs rename to src/utils/errors.rs diff --git a/backend/src/utils/hash.rs b/src/utils/hash.rs similarity index 100% rename from backend/src/utils/hash.rs rename to src/utils/hash.rs diff --git a/backend/src/utils/helpers.rs b/src/utils/helpers.rs similarity index 100% rename from backend/src/utils/helpers.rs rename to src/utils/helpers.rs diff --git a/backend/src/utils/mod.rs b/src/utils/mod.rs similarity index 100% rename from backend/src/utils/mod.rs rename to src/utils/mod.rs diff --git a/backend/src/utils/validations.rs b/src/utils/validations.rs similarity index 100% rename from backend/src/utils/validations.rs rename to src/utils/validations.rs From 0ed6bbdc6024546183369efef6979d0527882d3a Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 22 May 2023 12:10:05 +0530 Subject: [PATCH 011/352] refactor: moved db related modules to db crate messages and handlers are used to setup Actors for db calls, hence these should be kept inside DB crate --- .rustfmt.toml | 1 + flake.nix | 8 +- src/api/derived/config.rs | 130 ++++---- src/api/derived/context_override.rs | 41 ++- src/api/derived/mod.rs | 2 +- src/api/derived/promote.rs | 52 ++-- src/api/derived/reduce.rs | 32 +- src/api/mod.rs | 2 +- src/api/primary/context_overrides.rs | 136 +++++--- src/api/primary/contexts.rs | 124 ++++---- src/api/primary/dimensions.rs | 77 ++--- src/api/primary/global_config.rs | 98 +++--- src/api/primary/mod.rs | 8 +- src/api/primary/new_contexts.rs | 292 ++++++++++-------- src/api/primary/overrides.rs | 115 ++++--- src/{ => db}/handlers/context_overrides.rs | 34 +- src/{ => db}/handlers/contexts.rs | 21 +- src/{ => db}/handlers/dimensions.rs | 38 +-- src/{ => db}/handlers/global_config.rs | 37 +-- src/{ => db}/handlers/mod.rs | 6 +- src/{ => db}/handlers/new_contexts.rs | 60 ++-- src/{ => db}/handlers/overrides.rs | 28 +- src/{ => db}/messages/context_overrides.rs | 15 +- src/{ => db}/messages/contexts.rs | 9 +- src/{ => db}/messages/dimensions.rs | 7 +- src/{ => db}/messages/global_config.rs | 7 +- src/{ => db}/messages/mod.rs | 6 +- src/{ => db}/messages/new_contexts.rs | 14 +- src/{ => db}/messages/overrides.rs | 11 +- src/db/mod.rs | 5 +- src/{ => db}/models/db_models.rs | 9 +- .../models/insertables/context_overrides.rs | 6 +- src/{ => db}/models/insertables/contexts.rs | 4 +- src/{ => db}/models/insertables/dimensions.rs | 0 .../models/insertables/global_config.rs | 0 src/{ => db}/models/insertables/mod.rs | 6 +- .../models/insertables/new_contexts.rs | 2 +- src/{ => db}/models/insertables/overrides.rs | 4 +- src/{ => db}/models/mod.rs | 0 src/db/utils.rs | 9 +- src/main.rs | 155 ++++------ src/utils/errors.rs | 37 ++- src/utils/hash.rs | 10 +- src/utils/helpers.rs | 40 ++- src/utils/mod.rs | 2 +- src/utils/validations.rs | 42 +-- 46 files changed, 915 insertions(+), 827 deletions(-) create mode 100644 .rustfmt.toml rename src/{ => db}/handlers/context_overrides.rs (62%) rename src/{ => db}/handlers/contexts.rs (65%) rename src/{ => db}/handlers/dimensions.rs (53%) rename src/{ => db}/handlers/global_config.rs (54%) rename src/{ => db}/handlers/mod.rs (82%) rename src/{ => db}/handlers/new_contexts.rs (56%) rename src/{ => db}/handlers/overrides.rs (64%) rename src/{ => db}/messages/context_overrides.rs (55%) rename src/{ => db}/messages/contexts.rs (61%) rename src/{ => db}/messages/dimensions.rs (79%) rename src/{ => db}/messages/global_config.rs (80%) rename src/{ => db}/messages/mod.rs (82%) rename src/{ => db}/messages/new_contexts.rs (71%) rename src/{ => db}/messages/overrides.rs (60%) rename src/{ => db}/models/db_models.rs (95%) rename src/{ => db}/models/insertables/context_overrides.rs (76%) rename src/{ => db}/models/insertables/contexts.rs (79%) rename src/{ => db}/models/insertables/dimensions.rs (100%) rename src/{ => db}/models/insertables/global_config.rs (100%) rename src/{ => db}/models/insertables/mod.rs (82%) rename src/{ => db}/models/insertables/new_contexts.rs (94%) rename src/{ => db}/models/insertables/overrides.rs (82%) rename src/{ => db}/models/mod.rs (100%) diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..1a076d889 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +edition="2018" diff --git a/flake.nix b/flake.nix index f08f13f91..9343ffed1 100644 --- a/flake.nix +++ b/flake.nix @@ -25,14 +25,18 @@ devShell = pkgs.mkShell { nativeBuildInputs = with pkgs; [ + # Build requirements rustc cargo - postgresql_12 libiconv openssl - docker-compose + postgresql_12 + # Extras rust-analyzer rustfmt + bacon + cargo-watch + docker-compose ]; # buildInputs = with pkgs; [ ]; }; diff --git a/src/api/derived/config.rs b/src/api/derived/config.rs index df9beeaf5..f9c289b65 100644 --- a/src/api/derived/config.rs +++ b/src/api/derived/config.rs @@ -3,68 +3,56 @@ use std::collections::HashMap; use actix_web::{ - Either::Left, get, web::{Data, Json}, + Either::Left, HttpRequest, }; use serde_json::{to_value, Error, Value}; use crate::api::primary::{ - context_overrides::fetch_override_from_ctx_id, - global_config::get_complete_config, - contexts::fetch_context, - new_contexts::fetch_new_contexts, + context_overrides::fetch_override_from_ctx_id, contexts::fetch_context, + global_config::get_complete_config, new_contexts::fetch_new_contexts, overrides::get_override_helper, }; use crate::utils::{ errors::{ AppError, - AppErrorType::{ - DBError, - SomethingWentWrong, - } + AppErrorType::{DBError, SomethingWentWrong}, }, hash::string_based_b64_hash, - helpers::{ - create_all_unique_subsets, - split_stringified_key_value_pair, - strip_double_quotes - }, + helpers::{create_all_unique_subsets, split_stringified_key_value_pair, strip_double_quotes}, }; use crate::AppState; -fn default_parsing_error(err: Error) -> AppError{ +fn default_parsing_error(err: Error) -> AppError { AppError { message: None, cause: Some(Left(err.to_string())), - status: SomethingWentWrong + status: SomethingWentWrong, } } - -async fn _get_context_overrides_object(state: &Data, query_string: &str) -> Result { +async fn _get_context_overrides_object( + state: &Data, + query_string: &str, +) -> Result { if query_string == "" { return Ok(Value::default()); } -/************************************************************************************************************/ + /************************************************************************************************************/ // ! Optimize this section - let conditions_vector_temp: Vec = - split_stringified_key_value_pair(&query_string) + let conditions_vector_temp: Vec = split_stringified_key_value_pair(&query_string) .iter() .map(|x| x.join("=")) .collect(); - let conditions_vector: Vec<&str> = - conditions_vector_temp - .iter() - .map(|s| &**s) - .collect(); + let conditions_vector: Vec<&str> = conditions_vector_temp.iter().map(|s| &**s).collect(); -/************************************************************************************************************/ + /************************************************************************************************************/ let keys = create_all_unique_subsets(&conditions_vector); @@ -78,30 +66,38 @@ async fn _get_context_overrides_object(state: &Data, query_string: &st let key_string = item.to_owned().join("&"); let hashed_key = string_based_b64_hash(&key_string).to_string(); - if let Ok(override_id) = fetch_override_from_ctx_id(&state, &hashed_key).await { - - let fetched_override_value = get_override_helper(&state, override_id.to_owned()).await?; + let fetched_override_value = + get_override_helper(&state, override_id.to_owned()).await?; override_map.insert(override_id.to_owned(), fetched_override_value); contexts.push( to_value(HashMap::from([ - ("overrideWithKeys", to_value(override_id).map_err(default_parsing_error)?), + ( + "overrideWithKeys", + to_value(override_id).map_err(default_parsing_error)?, + ), ("condition", fetch_context(&state, &hashed_key).await?), ])) .map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), status: DBError, - })? + })?, ); } } to_value(HashMap::from([ - ("context", to_value(contexts).map_err(default_parsing_error)?), - ("overrides", to_value(override_map).map_err(default_parsing_error)?), + ( + "context", + to_value(contexts).map_err(default_parsing_error)?, + ), + ( + "overrides", + to_value(override_map).map_err(default_parsing_error)?, + ), ])) .map_err(|err| AppError { message: None, @@ -110,71 +106,81 @@ async fn _get_context_overrides_object(state: &Data, query_string: &st }) } - -async fn get_new_context_overrides_object (state: &Data, query_string: &str) -> Result { +async fn get_new_context_overrides_object( + state: &Data, + query_string: &str, +) -> Result { let raw_contexts = fetch_new_contexts(state, query_string.to_string()).await?; - let mut contexts = Vec::new(); let mut override_map = HashMap::new(); for item in raw_contexts { - match (item.get("id"), item.get("condition")) { (Some(context_id), Some(condition)) => { - if let Ok(override_id) = - fetch_override_from_ctx_id( - &state, - strip_double_quotes(&context_id.to_string()) - ).await { - - let fetched_override_value = get_override_helper(&state, override_id.to_owned()).await?; + fetch_override_from_ctx_id(&state, strip_double_quotes(&context_id.to_string())) + .await + { + let fetched_override_value = + get_override_helper(&state, override_id.to_owned()).await?; override_map.insert(override_id.to_owned(), fetched_override_value); contexts.push( to_value(HashMap::from([ - ("overrideWithKeys", &to_value(override_id).map_err(default_parsing_error)?), + ( + "overrideWithKeys", + &to_value(override_id).map_err(default_parsing_error)?, + ), ("condition", condition), ])) .map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), status: DBError, - })? + })?, ); } - - }, + } (_, _) => (), } } to_value(HashMap::from([ - ("context", to_value(contexts).map_err(default_parsing_error)?), - ("overrides", to_value(override_map).map_err(default_parsing_error)?), + ( + "context", + to_value(contexts).map_err(default_parsing_error)?, + ), + ( + "overrides", + to_value(override_map).map_err(default_parsing_error)?, + ), ])) .map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), status: DBError, }) - } - #[get("")] pub async fn get_config(state: Data, req: HttpRequest) -> Result, AppError> { let query_string = req.query_string(); - Ok(Json(to_value(HashMap::from([ - ("global_config", get_complete_config(&state).await?), - // ("context_overrides", get_context_overrides_object(&state, query_string).await?) - ("context_overrides", get_new_context_overrides_object(&state, query_string).await?) - ])).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?)) + Ok(Json( + to_value(HashMap::from([ + ("global_config", get_complete_config(&state).await?), + // ("context_overrides", get_context_overrides_object(&state, query_string).await?) + ( + "context_overrides", + get_new_context_overrides_object(&state, query_string).await?, + ), + ])) + .map_err(|err| AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + })?, + )) } diff --git a/src/api/derived/context_override.rs b/src/api/derived/context_override.rs index 1e55b83bb..77e04e868 100644 --- a/src/api/derived/context_override.rs +++ b/src/api/derived/context_override.rs @@ -2,31 +2,25 @@ use std::collections::HashMap; use actix_web::{ - Either::Left, post, web::{Data, Json}, + Either::Left, }; use log::info; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use serde_json::{to_value, Value}; -use crate::utils::errors::{ - AppError, - AppErrorType::{ - SomethingWentWrong, - } -}; +use crate::utils::errors::{AppError, AppErrorType::SomethingWentWrong}; use crate::api::primary::{ - overrides::add_new_override, // contexts::add_new_context, context_overrides::add_ctx_override, - new_contexts::add_new_context_v2 + new_contexts::add_new_context_v2, + overrides::add_new_override, }; - -use crate::{AppState}; +use crate::AppState; #[derive(Deserialize, Serialize)] pub struct KeyValue { @@ -35,11 +29,13 @@ pub struct KeyValue { } #[post("")] -pub async fn add_new_context_override(state: Data, body: Json) -> Result, AppError> { +pub async fn add_new_context_override( + state: Data, + body: Json, +) -> Result, AppError> { let override_value_from_body = body.override_value.clone(); let context_value_from_body = body.context_value.clone(); - info!("Prithiv {:?}", context_value_from_body); // ! Create transaction @@ -55,15 +51,14 @@ pub async fn add_new_context_override(state: Data, body: Json AppError { AppError { message: None, cause: Some(Left(err.to_string())), - status: SomethingWentWrong + status: SomethingWentWrong, } } @@ -37,25 +35,28 @@ pub struct KeyValue { } #[post("")] -pub async fn promote_contexts_overrides(input_state: Data, body: Json) -> Result, AppError> { +pub async fn promote_contexts_overrides( + input_state: Data, + body: Json, +) -> Result, AppError> { let state = &input_state; let input_context = body.context_value.clone(); let dimension_to_be_dropped = body.dimension_to_be_dropped.clone(); - let get_processed_context_and_hash_values = |keys_to_be_excluded: Option<&Vec>| -> Result<(String, Value), AppError>{ - let (processed_context_value, _) = - process_input_context_json(&input_context, keys_to_be_excluded)?; - let context_value = - to_value(processed_context_value).map_err(default_parsing_error)?; - let hashed_value = - string_based_b64_hash(context_value.to_string()).to_string(); + let get_processed_context_and_hash_values = + |keys_to_be_excluded: Option<&Vec>| -> Result<(String, Value), AppError> { + let (processed_context_value, _) = + process_input_context_json(&input_context, keys_to_be_excluded)?; + let context_value = to_value(processed_context_value).map_err(default_parsing_error)?; + let hashed_value = string_based_b64_hash(context_value.to_string()).to_string(); - Ok((hashed_value, context_value)) - }; + Ok((hashed_value, context_value)) + }; let (existing_hashed_value, _) = get_processed_context_and_hash_values(None)?; - let (_, new_context_value) = get_processed_context_and_hash_values(Some(&Vec::from([dimension_to_be_dropped])))?; + let (_, new_context_value) = + get_processed_context_and_hash_values(Some(&Vec::from([dimension_to_be_dropped])))?; // Add transaction wrapper here let override_id = fetch_override_from_ctx_id(&state, &existing_hashed_value).await?; @@ -64,5 +65,4 @@ pub async fn promote_contexts_overrides(input_state: Data, body: Json< delete_new_context_helper(&state, existing_hashed_value.clone()).await?; delete_ctx_override_helper(&state, existing_hashed_value).await?; add_ctx_override(&state, context_response.id, override_id, true).await - -} \ No newline at end of file +} diff --git a/src/api/derived/reduce.rs b/src/api/derived/reduce.rs index 8ea776b92..8a94da376 100644 --- a/src/api/derived/reduce.rs +++ b/src/api/derived/reduce.rs @@ -9,19 +9,16 @@ use actix_web::{ use crate::utils::errors::AppError; use crate::api::primary::{ - overrides::{delete_override_helper, get_all_overrides}, context_overrides::fetch_all_ctx_overrides, - new_contexts::{delete_new_context_helper, fetch_all_new_contexts} + new_contexts::{delete_new_context_helper, fetch_all_new_contexts}, + overrides::{delete_override_helper, get_all_overrides}, }; - use crate::AppState; use std::iter::FromIterator; - fn find_diff(all_keys: &Vec, sub_set_keys: &Vec) -> Vec { - let a_set: HashSet = HashSet::from_iter(sub_set_keys.iter().cloned()); let mut difference = Vec::new(); @@ -35,37 +32,36 @@ fn find_diff(all_keys: &Vec, sub_set_keys: &Vec) -> Vec } #[delete("")] -pub async fn reduce_contexts_overrides(input_state: Data) -> Result, AppError> { +pub async fn reduce_contexts_overrides( + input_state: Data, +) -> Result, AppError> { let state = &input_state; let ctx_overrides = fetch_all_ctx_overrides(state).await?; - let mapped_context_ids: Vec = - ctx_overrides + let mapped_context_ids: Vec = ctx_overrides .iter() .map(|i| i.context_id.to_owned()) .collect(); - let mapped_override_ids: Vec = - ctx_overrides + let mapped_override_ids: Vec = ctx_overrides .iter() .map(|i| i.override_id.to_owned()) .collect(); - - let all_context_ids: Vec = - fetch_all_new_contexts(state).await? + let all_context_ids: Vec = fetch_all_new_contexts(state) + .await? .iter() .map(|x| x.key.to_owned()) .collect(); - let all_override_ids: Vec = - get_all_overrides(state).await? + let all_override_ids: Vec = get_all_overrides(state) + .await? .iter() .map(|x| x.key.to_owned()) .collect(); - let extra_context_ids = find_diff(&all_context_ids, &mapped_context_ids); - let extra_override_ids = find_diff(&all_override_ids, &mapped_override_ids); + let extra_context_ids = find_diff(&all_context_ids, &mapped_context_ids); + let extra_override_ids = find_diff(&all_override_ids, &mapped_override_ids); for item in extra_context_ids { delete_new_context_helper(&state, item).await?; @@ -76,4 +72,4 @@ pub async fn reduce_contexts_overrides(input_state: Data) -> Result, context_id: String, override_id: String, return_if_present: bool) -> Result, AppError> { - +pub async fn add_ctx_override( + state: &Data, + context_id: String, + override_id: String, + return_if_present: bool, +) -> Result, AppError> { let db: Addr = state.db.clone(); match db - .send(CreateCtxOverrides {context_id: context_id.to_owned(), override_id}) + .send(CreateCtxOverrides { + context_id: context_id.to_owned(), + override_id, + }) .await { - Ok(Ok(result)) => Ok(Json(ContextOverrideResponse {context_id: result.context_id})), - Ok(Err(err)) => + Ok(Ok(result)) => Ok(Json(ContextOverrideResponse { + context_id: result.context_id, + })), + Ok(Err(err)) => { if return_if_present { - Ok(Json(ContextOverrideResponse {context_id})) + Ok(Json(ContextOverrideResponse { context_id })) } else { Err(AppError { message: Some("Data already exists".to_string()), cause: Some(Left(err.to_string())), - status: DataExists + status: DataExists, }) - }, - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) + } + } + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } -pub async fn fetch_override_from_ctx_id(state: &Data, context_id: &str) -> Result { +pub async fn fetch_override_from_ctx_id( + state: &Data, + context_id: &str, +) -> Result { let db: Addr = state.as_ref().db.clone(); match db .send(FetchCtxOverrides { - context_id: context_id.to_string() + context_id: context_id.to_string(), }) .await { @@ -75,56 +87,71 @@ pub async fn fetch_override_from_ctx_id(state: &Data, context_id: &str Ok(Err(err)) => Err(AppError { message: Some("failed to fetch key value".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } -pub async fn fetch_all_ctx_overrides(state: &Data) -> Result, AppError> { +pub async fn fetch_all_ctx_overrides( + state: &Data, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); - match db - .send(FetchAllCtxOverrides) - .await - { + match db.send(FetchAllCtxOverrides).await { Ok(Ok(result)) => Ok(result), Ok(Err(err)) => Err(AppError { message: Some("failed to fetch key value".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } #[post("")] -pub async fn post_ctx_override(state: Data, body: Json) -> Result, AppError> { +pub async fn post_ctx_override( + state: Data, + body: Json, +) -> Result, AppError> { let ctx_id: String = body.context_id.clone(); - let ovr_id : String = body.override_id.clone(); + let ovr_id: String = body.override_id.clone(); add_ctx_override(&state, ctx_id, ovr_id, false).await } #[get("/{id}")] -pub async fn get_ctx_override(state: Data, id: Path) -> Result, AppError> { +pub async fn get_ctx_override( + state: Data, + id: Path, +) -> Result, AppError> { let context_id = id.to_string(); let override_id = fetch_override_from_ctx_id(&state, &context_id).await?; Ok(Json( - to_value( - HashMap::from([ - ("context_id", context_id), - ("override_id", override_id) - ]) - ).map_err(|err| AppError { + to_value(HashMap::from([ + ("context_id", context_id), + ("override_id", override_id), + ])) + .map_err(|err| AppError { message: None, cause: Some(Left(err.to_string())), - status: DBError - })? + status: DBError, + })?, )) } -pub async fn delete_ctx_override_helper(state: &Data, id: String) -> Result, AppError> { +pub async fn delete_ctx_override_helper( + state: &Data, + id: String, +) -> Result, AppError> { let db: Addr = state.db.clone(); match db @@ -137,13 +164,20 @@ pub async fn delete_ctx_override_helper(state: &Data, id: String) -> R Ok(Err(err)) => Err(AppError { message: Some("Data not found for context override deletion".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } #[delete("/{id}")] -pub async fn delete_ctx_override(state: Data, id: Path) -> Result, AppError> { +pub async fn delete_ctx_override( + state: Data, + id: Path, +) -> Result, AppError> { delete_ctx_override_helper(&state, id.to_string()).await } diff --git a/src/api/primary/contexts.rs b/src/api/primary/contexts.rs index 30d5b03d0..adb8cb4b7 100644 --- a/src/api/primary/contexts.rs +++ b/src/api/primary/contexts.rs @@ -1,62 +1,52 @@ -use std::collections::{ - BTreeMap, - HashMap -}; +use std::collections::{BTreeMap, HashMap}; use actix::Addr; use actix_web::{ - Either::{Left}, - delete, - get, - post, + delete, get, post, web::{Data, Json, Path}, + Either::Left, }; use serde::Serialize; use serde_json::{from_value, to_value, Error, Value}; use crate::{ - messages::contexts::{CreateContext, DeleteContext, FetchContext}, + db::messages::contexts::{CreateContext, DeleteContext, FetchContext}, AppState, DbActor, }; use crate::utils::{ errors::{ AppError, - AppErrorType::{ - DataExists, - NotFound, - DBError, - SomethingWentWrong - } + AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, }, hash::string_based_b64_hash, helpers::split_stringified_key_value_pair, }; - #[derive(Serialize, Clone)] pub struct ContextIdResponse { pub id: String, } -fn default_parsing_error(err: Error) -> AppError{ +fn default_parsing_error(err: Error) -> AppError { AppError { message: None, cause: Some(Left(err.to_string())), - status: SomethingWentWrong + status: SomethingWentWrong, } } // TODO :: Implement Range based transforms fn transform_context(raw_context_value: Value) -> Result { - // BTreeMap is used to make keys in sorted order - let b_tree: BTreeMap = from_value(raw_context_value).map_err(default_parsing_error)?; + let b_tree: BTreeMap = + from_value(raw_context_value).map_err(default_parsing_error)?; let mut result: Vec = Vec::new(); for (key, value) in b_tree { - let value_object: HashMap = from_value(value).map_err(default_parsing_error)?; + let value_object: HashMap = + from_value(value).map_err(default_parsing_error)?; let operator = value_object.get("operator").map(|val| val.to_string()); let value = value_object.get("value").map(|val| val.to_string()); @@ -65,23 +55,25 @@ fn transform_context(raw_context_value: Value) -> Result { let min_range = value_object.get("min_range").map(|val| val.to_string()); match (operator.as_deref(), value, max_range, min_range) { - // ? `==` or `equals` ? (Some("=="), Some(val), _, _) => result.push(key + "=" + &val), // TODO :: Implement Range based transforms properly // ? Do we need to add inclusive check - (Some("range"), _, Some(max_range_val), Some(min_range_val)) => result.push(min_range_val + "<" + &key + "<" + &max_range_val), - (_, _, _, _) => () + (Some("range"), _, Some(max_range_val), Some(min_range_val)) => { + result.push(min_range_val + "<" + &key + "<" + &max_range_val) + } + (_, _, _, _) => (), }; - } Ok(result.join("&")) } - -pub async fn add_new_context(state: &Data, context_value: Value) -> Result { +pub async fn add_new_context( + state: &Data, + context_value: Value, +) -> Result { let db: Addr = state.db.clone(); let transformed_context_value = transform_context(context_value)?; @@ -97,34 +89,35 @@ pub async fn add_new_context(state: &Data, context_value: Value) -> Re }) .await { - Ok(Ok(result)) => Ok(ContextIdResponse {id: result.key}), + Ok(Ok(result)) => Ok(ContextIdResponse { id: result.key }), Ok(Err(err)) => Err(AppError { - message: Some("Failed to add context".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), + message: Some("Failed to add context".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } - fn format_context_json(input: &str) -> Result { let conditions_vector = split_stringified_key_value_pair(input); let mut formatted_conditions_vector = Vec::new(); for condition in conditions_vector { - let var_map = to_value(HashMap::from([("var", condition[0])])).map_err(default_parsing_error)?; + let var_map = + to_value(HashMap::from([("var", condition[0])])).map_err(default_parsing_error)?; let value_as_value = to_value(condition[1]).map_err(default_parsing_error)?; let value_arr = vec![[var_map, value_as_value]]; // Add range based queries - let condition_map = to_value(HashMap::from([("==", value_arr)])).map_err(default_parsing_error)?; + let condition_map = + to_value(HashMap::from([("==", value_arr)])).map_err(default_parsing_error)?; formatted_conditions_vector.push(condition_map); } @@ -132,40 +125,41 @@ fn format_context_json(input: &str) -> Result { Ok(if formatted_conditions_vector.len() == 1 { formatted_conditions_vector[0].to_owned() } else { - to_value(HashMap::from([("and", formatted_conditions_vector)])).map_err(default_parsing_error)? + to_value(HashMap::from([("and", formatted_conditions_vector)])) + .map_err(default_parsing_error)? }) } - -pub async fn fetch_context(state: &Data, key: &String) -> Result{ +pub async fn fetch_context(state: &Data, key: &String) -> Result { let db: Addr = state.db.clone(); let raw_context_string = match db .send(FetchContext { - key: key.to_owned() + key: key.to_owned(), }) .await { Ok(Ok(result)) => Ok(result.value), Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), + message: Some("Failed to get context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), }?; format_context_json(&raw_context_string) - } #[post("")] -pub async fn post_context(state: Data, body: Json) -> Result, AppError> { +pub async fn post_context( + state: Data, + body: Json, +) -> Result, AppError> { let context_value = body.clone(); Ok(Json(add_new_context(&state, context_value).await?)) } @@ -176,7 +170,10 @@ pub async fn get_context(state: Data, id: Path) -> Result, key: Path) -> Result, AppError> { +pub async fn delete_context( + state: Data, + key: Path, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); match db @@ -187,15 +184,14 @@ pub async fn delete_context(state: Data, key: Path) -> Result< { Ok(Ok(result)) => Ok(Json(result.value)), Ok(Err(err)) => Err(AppError { - message: Some("failed to remove context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), + message: Some("failed to remove context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } diff --git a/src/api/primary/dimensions.rs b/src/api/primary/dimensions.rs index 01812ce90..514696083 100644 --- a/src/api/primary/dimensions.rs +++ b/src/api/primary/dimensions.rs @@ -1,28 +1,21 @@ -use actix_web:: { - Either::{Left}, - get, - post, - web::{Path, Json, Data}, +use crate::db::models::db_models::Dimension; +use actix_web::{ + get, post, + web::{Data, Json, Path}, + Either::Left, }; -use crate::models::db_models::Dimension; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use crate::{ - messages::dimensions::{ - FetchDimensions, - FetchDimension, - CreateDimension - }, AppState, DbActor}; + db::messages::dimensions::{CreateDimension, FetchDimension, FetchDimensions}, + AppState, DbActor, +}; use actix::Addr; use crate::utils::errors::{ AppError, - AppErrorType::{ - DBError, - NotFound, - DataExists - } + AppErrorType::{DBError, DataExists, NotFound}, }; // Get dimension table @@ -39,15 +32,14 @@ pub async fn fetch_dimensions(state: &Data) -> Result, Ok(Err(err)) => Err(AppError { message: Some("failed to get dimensions".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, }), Err(err) => Err(AppError { message: None, cause: Some(Left(err.to_string())), - status: DBError - }) + status: DBError, + }), } - } // Get request to fetch dimension from dimension name @@ -57,26 +49,33 @@ pub struct Key { } #[get("/{dimension}")] -pub async fn get_dimension_key(state: Data, params: Path) -> Result, AppError> { +pub async fn get_dimension_key( + state: Data, + params: Path, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); let dimension_key = params.into_inner().dimension; - match db.send(FetchDimension {dimension: dimension_key}).await { + match db + .send(FetchDimension { + dimension: dimension_key, + }) + .await + { Ok(Ok(result)) => Ok(Json(result)), Ok(Err(err)) => Err(AppError { message: Some("failed to get required dimension".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, }), Err(err) => Err(AppError { message: None, cause: Some(Left(err.to_string())), - status: DBError - }) + status: DBError, + }), } } - // Post request to add key, value to dimension table #[derive(Deserialize, Serialize, Clone)] pub struct KeyValue { @@ -85,25 +84,29 @@ pub struct KeyValue { } #[post("")] -pub async fn post_dimension(state: Data, body: Json) -> Result, AppError> { +pub async fn post_dimension( + state: Data, + body: Json, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); - match db.send(CreateDimension { - dimension: body.dimension.clone(), - priority: body.priority - }).await { - + match db + .send(CreateDimension { + dimension: body.dimension.clone(), + priority: body.priority, + }) + .await + { Ok(Ok(result)) => Ok(Json(result)), Ok(Err(err)) => Err(AppError { message: Some("failed to add dimension".to_string()), cause: Some(Left(err.to_string())), - status: DataExists + status: DataExists, }), Err(err) => Err(AppError { message: None, cause: Some(Left(err.to_string())), - status: DBError - }) + status: DBError, + }), } } - diff --git a/src/api/primary/global_config.rs b/src/api/primary/global_config.rs index 04fe385a0..00753748a 100644 --- a/src/api/primary/global_config.rs +++ b/src/api/primary/global_config.rs @@ -1,47 +1,38 @@ -use std::collections::{ - BTreeMap, - HashMap -}; +use std::collections::{BTreeMap, HashMap}; -use actix_web:: { - Either::{Left}, - get, - post, - web::{Path, Json, Data}, +use crate::db::models::db_models::GlobalConfig; +use crate::utils::errors::{ + AppError, + AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, }; -use serde_json::{Value, from_value, to_value}; -use crate::models::db_models::GlobalConfig; -use serde::{Serialize, Deserialize}; use crate::{ - messages::global_config::{ - FetchGlobalConfig, - FetchConfigKey, - CreateGlobalKey, - }, AppState, DbActor}; + db::messages::global_config::{CreateGlobalKey, FetchConfigKey, FetchGlobalConfig}, + AppState, DbActor, +}; use actix::Addr; -use crate::utils::errors::{ - AppError, - AppErrorType::{ - SomethingWentWrong, - DBError, - NotFound, - DataExists - } +use actix_web::{ + get, post, + web::{Data, Json, Path}, + Either::Left, }; +use serde::{Deserialize, Serialize}; +use serde_json::{from_value, to_value, Value}; -async fn get_all_rows_from_global_config(state: &Data) -> Result, AppError> { +async fn get_all_rows_from_global_config( + state: &Data, +) -> Result, AppError> { let db: Addr = state.db.clone(); match db.send(FetchGlobalConfig).await { Ok(Ok(result)) => Ok(result), Ok(Err(err)) => Err(AppError { message: Some("config not found".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, }), Err(err) => Err(AppError { message: None, cause: Some(Left(err.to_string())), - status: DBError + status: DBError, }), } } @@ -54,15 +45,13 @@ pub async fn get_complete_config(state: &Data) -> Result) -> Result, AppError> { @@ -76,43 +65,54 @@ pub struct Key { } #[get("/{key}")] -pub async fn get_global_config_key(state: Data, params: Path) -> Result, AppError> { +pub async fn get_global_config_key( + state: Data, + params: Path, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); let key = params.into_inner().key; - match db.send(FetchConfigKey {key}).await { + match db.send(FetchConfigKey { key }).await { Ok(Ok(result)) => Ok(Json(result)), Ok(Err(err)) => Err(AppError { message: Some("Failed to fetch global config".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } - #[post("")] -pub async fn post_config_key_value(state: Data, body: Json) -> Result, AppError> { +pub async fn post_config_key_value( + state: Data, + body: Json, +) -> Result, AppError> { let db: Addr = state.as_ref().db.clone(); - let b_tree: BTreeMap = - from_value(body.clone()) - .map_err(|err| AppError {message: None, cause: Some(Left(err.to_string())), status: DBError})?; + let b_tree: BTreeMap = from_value(body.clone()).map_err(|err| AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + })?; for (key, value) in b_tree { match db.send(CreateGlobalKey { key, value }).await { Ok(Ok(result)) => Ok(Json(result)), Ok(Err(err)) => Err(AppError { - message: Some("Failed to add new key to global config".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), + message: Some("Failed to add new key to global config".to_string()), + cause: Some(Left(err.to_string())), + status: DataExists, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), }?; } diff --git a/src/api/primary/mod.rs b/src/api/primary/mod.rs index 95f5a38b8..7b9cd61da 100644 --- a/src/api/primary/mod.rs +++ b/src/api/primary/mod.rs @@ -1,6 +1,6 @@ -pub mod global_config; +pub mod context_overrides; +pub mod contexts; pub mod dimensions; +pub mod global_config; +pub mod new_contexts; pub mod overrides; -pub mod contexts; -pub mod context_overrides; -pub mod new_contexts; \ No newline at end of file diff --git a/src/api/primary/new_contexts.rs b/src/api/primary/new_contexts.rs index 708f13532..70fdffe6b 100644 --- a/src/api/primary/new_contexts.rs +++ b/src/api/primary/new_contexts.rs @@ -1,45 +1,32 @@ -use std::{ - cmp::Ordering, - collections::HashMap -}; +use std::{cmp::Ordering, collections::HashMap}; use actix::Addr; use actix_web::{ - Either::{Left}, - delete, - get, - post, + delete, get, post, web::{Data, Json, Path}, + Either::Left, HttpRequest, }; use serde::Serialize; use serde_json::{from_value, to_value, Error, Value}; use crate::{ - messages::new_contexts::{CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext}, - api::primary::{ - dimensions::fetch_dimensions + api::primary::dimensions::fetch_dimensions, + db::messages::new_contexts::{ + CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext, }, - AppState, DbActor + AppState, DbActor, }; -use crate::models::db_models::NewContexts; +use crate::db::models::db_models::NewContexts; use crate::utils::{ errors::{ AppError, - AppErrorType::{ - DataExists, - NotFound, - DBError, - SomethingWentWrong - } + AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, }, hash::string_based_b64_hash, - helpers::{ - split_stringified_key_value_pair, - strip_double_quotes, - }, + helpers::{split_stringified_key_value_pair, strip_double_quotes}, }; #[derive(Serialize, Clone)] @@ -51,11 +38,11 @@ fn default_parsing_error(err: Error) -> AppError { AppError { message: None, cause: Some(Left(err.to_string())), - status: SomethingWentWrong + status: SomethingWentWrong, } } -fn get_dimension_name (idx: usize) -> String { +fn get_dimension_name(idx: usize) -> String { let dimesions = Vec::from(["tier", "merchantId", "os", "country"]); dimesions[idx].to_string() } @@ -63,10 +50,10 @@ fn get_dimension_name (idx: usize) -> String { type ContextValueAndDimensionMap = (HashMap, HashMap); type MaybeContextValueAndDimensionMap = (Option>, HashMap); -fn contexts_comparator(priority_map: HashMap) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { - +fn contexts_comparator( + priority_map: HashMap, +) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { let get_value_from_map = move |key: Option| { - if let Some(val) = key { if let Some(val1) = priority_map.get(&val) { return val1.to_owned(); @@ -77,32 +64,28 @@ fn contexts_comparator(priority_map: HashMap) -> impl Fn(&NewContex }; move |ctx1: &NewContexts, ctx2: &NewContexts| { - let val1 = - get_value_from_map(ctx1.column1.to_owned().map(|_| get_dimension_name(0))) + - get_value_from_map(ctx1.column2.to_owned().map(|_| get_dimension_name(1))) + - get_value_from_map(ctx1.column3.to_owned().map(|_| get_dimension_name(2))) + - get_value_from_map(ctx1.column4.to_owned().map(|_| get_dimension_name(3))); - - let val2 = - get_value_from_map(ctx2.column1.to_owned().map(|_| get_dimension_name(0))) + - get_value_from_map(ctx2.column2.to_owned().map(|_| get_dimension_name(1))) + - get_value_from_map(ctx2.column3.to_owned().map(|_| get_dimension_name(2))) + - get_value_from_map(ctx2.column4.to_owned().map(|_| get_dimension_name(3))); + let val1 = get_value_from_map(ctx1.column1.to_owned().map(|_| get_dimension_name(0))) + + get_value_from_map(ctx1.column2.to_owned().map(|_| get_dimension_name(1))) + + get_value_from_map(ctx1.column3.to_owned().map(|_| get_dimension_name(2))) + + get_value_from_map(ctx1.column4.to_owned().map(|_| get_dimension_name(3))); + + let val2 = get_value_from_map(ctx2.column1.to_owned().map(|_| get_dimension_name(0))) + + get_value_from_map(ctx2.column2.to_owned().map(|_| get_dimension_name(1))) + + get_value_from_map(ctx2.column3.to_owned().map(|_| get_dimension_name(2))) + + get_value_from_map(ctx2.column4.to_owned().map(|_| get_dimension_name(3))); val1.cmp(&val2) } } - pub async fn fetch_raw_context_v2( state: &Data, column1: Option<&String>, column2: Option<&String>, column3: Option<&String>, column4: Option<&String>, - dimesion_map: HashMap + dimesion_map: HashMap, ) -> Result, AppError> { - let db: Addr = state.db.clone(); let result: Vec = match db @@ -116,20 +99,18 @@ pub async fn fetch_raw_context_v2( { Ok(Ok(res)) => Ok(res), Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), + message: Some("Failed to get context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), }?; - - let mut final_result: Vec = - result + let mut final_result: Vec = result .into_iter() .filter(|y| { let x = y.to_owned(); @@ -138,23 +119,30 @@ pub async fn fetch_raw_context_v2( let column3_from_row: Option = x.column3.to_owned(); let column4_from_row: Option = x.column4.to_owned(); - - (column1_from_row.is_none() || column1.is_none() || column1_from_row == column1.map(|x| x.to_string())) && - (column2_from_row.is_none() || column2.is_none() || column2_from_row == column2.map(|x| x.to_string())) && - (column3_from_row.is_none() || column3.is_none() || column3_from_row == column3.map(|x| x.to_string())) && - (column4_from_row.is_none() || column4.is_none() || column4_from_row == column4.map(|x| x.to_string())) + (column1_from_row.is_none() + || column1.is_none() + || column1_from_row == column1.map(|x| x.to_string())) + && (column2_from_row.is_none() + || column2.is_none() + || column2_from_row == column2.map(|x| x.to_string())) + && (column3_from_row.is_none() + || column3.is_none() + || column3_from_row == column3.map(|x| x.to_string())) + && (column4_from_row.is_none() + || column4.is_none() + || column4_from_row == column4.map(|x| x.to_string())) }) .collect(); final_result.sort_by(contexts_comparator(dimesion_map)); Ok(final_result) - } - -pub fn process_single_condition(value_object: &HashMap, keys_to_be_excluded: Option<&Vec>) -> MaybeContextValueAndDimensionMap { - +pub fn process_single_condition( + value_object: &HashMap, + keys_to_be_excluded: Option<&Vec>, +) -> MaybeContextValueAndDimensionMap { let mut result_map: HashMap = HashMap::new(); let mut result_context: Option> = None; @@ -164,22 +152,20 @@ pub fn process_single_condition(value_object: &HashMap, keys_to_b let variable_array: Vec = from_value(variable_array_value.to_owned()).unwrap(); if variable_array.len() >= 2 { - let variable_map: HashMap = from_value(variable_array[0].to_owned()).unwrap(); + let variable_map: HashMap = + from_value(variable_array[0].to_owned()).unwrap(); let mapped_value = variable_array[1].to_string(); if let Some(key) = variable_map.get("var") { let key_string = strip_double_quotes(&key.to_string()).to_string(); let to_be_included = - keys_to_be_excluded.map_or( - true, - |vect| !vect.contains(&key_string) - ); + keys_to_be_excluded.map_or(true, |vect| !vect.contains(&key_string)); if to_be_included { result_map.insert( // Removing double quotes at the start and end for both key and value strip_double_quotes(&key_string).to_owned(), - strip_double_quotes(&mapped_value).to_owned() + strip_double_quotes(&mapped_value).to_owned(), ); result_context = Some(value_object.to_owned()); @@ -191,9 +177,12 @@ pub fn process_single_condition(value_object: &HashMap, keys_to_b (result_context, result_map) } -pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Option<&Vec>) -> Result { - - let input_as_map: HashMap = from_value(input_json.to_owned()).map_err(default_parsing_error)?; +pub fn process_input_context_json( + input_json: &Value, + keys_to_be_excluded: Option<&Vec>, +) -> Result { + let input_as_map: HashMap = + from_value(input_json.to_owned()).map_err(default_parsing_error)?; let mut result_context_array = Vec::new(); // Single dimension or condition @@ -202,7 +191,7 @@ pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Optio return Err(AppError { message: None, cause: Some(Left("Case yet to handled properly".to_string())), - status: SomethingWentWrong + status: SomethingWentWrong, }); // return Ok(process_single_condition(&input_as_map, keys_to_be_excluded)); } @@ -211,9 +200,9 @@ pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Optio let val = input_as_map.get("and").unwrap(); let multiple_condition_array: Vec = from_value(val.to_owned()).unwrap(); - for item in multiple_condition_array { - let value_object: HashMap = from_value(item).map_err(default_parsing_error)?; + let value_object: HashMap = + from_value(item).map_err(default_parsing_error)?; let (maybe_ctx, map) = process_single_condition(&value_object, keys_to_be_excluded); if let Some(ctx) = maybe_ctx { @@ -222,12 +211,20 @@ pub fn process_input_context_json(input_json: &Value, keys_to_be_excluded: Optio } } - Ok((HashMap::from([ - ("and".to_string(), to_value(result_context_array).map_err(default_parsing_error)?) - ]), result_map)) + Ok(( + HashMap::from([( + "and".to_string(), + to_value(result_context_array).map_err(default_parsing_error)?, + )]), + result_map, + )) } -pub async fn add_new_context_v2(state: &Data, raw_context_value: Value, return_if_present: bool) -> Result { +pub async fn add_new_context_v2( + state: &Data, + raw_context_value: Value, + return_if_present: bool, +) -> Result { let db: Addr = state.db.clone(); let (processed_context_value, key_value_map) = @@ -245,29 +242,38 @@ pub async fn add_new_context_v2(state: &Data, raw_context_value: Value .send(CreateNewContext { key: hashed_value.to_owned(), value: context_value, - column1: key_value_map.get(&get_dimension_name(0)).map(|x| x.to_string()), - column2: key_value_map.get(&get_dimension_name(1)).map(|x| x.to_string()), - column3: key_value_map.get(&get_dimension_name(2)).map(|x| x.to_string()), - column4: key_value_map.get(&get_dimension_name(3)).map(|x| x.to_string()), + column1: key_value_map + .get(&get_dimension_name(0)) + .map(|x| x.to_string()), + column2: key_value_map + .get(&get_dimension_name(1)) + .map(|x| x.to_string()), + column3: key_value_map + .get(&get_dimension_name(2)) + .map(|x| x.to_string()), + column4: key_value_map + .get(&get_dimension_name(3)) + .map(|x| x.to_string()), }) .await { - Ok(Ok(result)) => Ok(ContextIdResponse {id: result.key}), - Ok(Err(err)) => + Ok(Ok(result)) => Ok(ContextIdResponse { id: result.key }), + Ok(Err(err)) => { if return_if_present { - Ok(ContextIdResponse {id: hashed_value}) + Ok(ContextIdResponse { id: hashed_value }) } else { Err(AppError { message: Some("Failed to add context".to_string()), cause: Some(Left(err.to_string())), - status: DataExists + status: DataExists, }) - }, + } + } Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } @@ -277,37 +283,43 @@ pub async fn fetch_all_new_contexts(state: &Data) -> Result Ok(res), Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), + message: Some("Failed to get context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } -pub async fn delete_new_context_helper(state: &Data, key: String) -> Result { +pub async fn delete_new_context_helper( + state: &Data, + key: String, +) -> Result { let db: Addr = state.db.clone(); - match db.send(DeleteNewContext {key}).await { + match db.send(DeleteNewContext { key }).await { Ok(Ok(res)) => Ok(res), Ok(Err(err)) => Err(AppError { - message: Some("Failed to delete context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), + message: Some("Failed to delete context".to_string()), + cause: Some(Left(err.to_string())), + status: NotFound, + }), Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } -pub async fn fetch_new_contexts(state: &Data, query_string: String) -> Result>, AppError> { +pub async fn fetch_new_contexts( + state: &Data, + query_string: String, +) -> Result>, AppError> { let key_value_pairs = split_stringified_key_value_pair(&query_string); let all_dimesions = fetch_dimensions(&state).await?; @@ -326,53 +338,63 @@ pub async fn fetch_new_contexts(state: &Data, query_string: String) -> } } - let raw_contexts = - fetch_raw_context_v2( - &state, - filter_input_keys_map.get(&get_dimension_name(0)), - filter_input_keys_map.get(&get_dimension_name(1)), - filter_input_keys_map.get(&get_dimension_name(2)), - filter_input_keys_map.get(&get_dimension_name(3)), - dimesion_map - ).await?; + let raw_contexts = fetch_raw_context_v2( + &state, + filter_input_keys_map.get(&get_dimension_name(0)), + filter_input_keys_map.get(&get_dimension_name(1)), + filter_input_keys_map.get(&get_dimension_name(2)), + filter_input_keys_map.get(&get_dimension_name(3)), + dimesion_map, + ) + .await?; let mut formatted_contexts = Vec::new(); for item in raw_contexts { - formatted_contexts.push( - HashMap::from([ - ("condition", item.value), - ("id", to_value(item.key).map_err(default_parsing_error)?), - ]) - ); + formatted_contexts.push(HashMap::from([ + ("condition", item.value), + ("id", to_value(item.key).map_err(default_parsing_error)?), + ])); } Ok(formatted_contexts) } - #[get("")] -pub async fn get_new_context(state: Data, req: HttpRequest) -> Result, AppError> { +pub async fn get_new_context( + state: Data, + req: HttpRequest, +) -> Result, AppError> { let query_string = req.query_string(); let formatted_contexts = fetch_new_contexts(&state, query_string.to_string()).await?; let mut result = Vec::new(); for item in &formatted_contexts { - result.push(HashMap::from([ - ("condition", item.get("condition").to_owned()) - ])); + result.push(HashMap::from([( + "condition", + item.get("condition").to_owned(), + )])); } Ok(Json(to_value(result).map_err(default_parsing_error)?)) } #[post("")] -pub async fn post_new_context(state: Data, body: Json) -> Result, AppError> { +pub async fn post_new_context( + state: Data, + body: Json, +) -> Result, AppError> { let context_value = body.clone(); - Ok(Json(add_new_context_v2(&state, context_value, false).await?)) + Ok(Json( + add_new_context_v2(&state, context_value, false).await?, + )) } - #[delete("/{id}")] -pub async fn delete_new_context(state: Data, id: Path) -> Result, AppError> { - Ok(Json(delete_new_context_helper(&state, id.to_string()).await?)) +pub async fn delete_new_context( + state: Data, + id: Path, +) -> Result, AppError> { + Ok(Json( + delete_new_context_helper(&state, id.to_string()).await?, + )) } diff --git a/src/api/primary/overrides.rs b/src/api/primary/overrides.rs index d75501140..2cbcf568c 100644 --- a/src/api/primary/overrides.rs +++ b/src/api/primary/overrides.rs @@ -1,29 +1,22 @@ use actix::Addr; use actix_web::{ - delete, - get, - post, + delete, get, post, web::{Data, Json, Path}, - Either::{Left, Right} + Either::{Left, Right}, }; use serde::Serialize; -use serde_json::{Value}; +use serde_json::Value; use crate::{ - messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}, - AppState, DbActor, models::db_models::Overrides, + db::messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}, + db::models::db_models::Overrides, + AppState, DbActor, }; use crate::utils::{ errors::{ AppError, - AppErrorType::{ - BadRequest, - DataExists, - DBError, - SomethingWentWrong, - NotFound - } + AppErrorType::{BadRequest, DBError, DataExists, NotFound, SomethingWentWrong}, }, hash::string_based_b64_hash, helpers::sort_multi_level_keys_in_stringified_json, @@ -37,8 +30,11 @@ pub struct OverrideIdResponse { pub id: String, } - -pub async fn add_new_override(state: &Data, override_value: Value, return_if_present: bool) -> Result { +pub async fn add_new_override( + state: &Data, + override_value: Value, + return_if_present: bool, +) -> Result { let db: Addr = state.db.clone(); let global_config_as_value = get_complete_config(&state).await?; @@ -47,19 +43,18 @@ pub async fn add_new_override(state: &Data, override_value: Value, ret return Err(AppError { message: Some("Validation failed".to_string()), cause: Some(Right(error_message)), - status: BadRequest - }) + status: BadRequest, + }); } // TODO :: Post as an array of value - let formatted_value = - sort_multi_level_keys_in_stringified_json(override_value) + let formatted_value = sort_multi_level_keys_in_stringified_json(override_value) // TODO :: Fix this properly // .ok_or(OverrideError::ErrorOnParsingBody {error_message : to_value("Error on sorting keys".to_string())})?; .ok_or(AppError { message: Some("Unable to parse override value".to_string()), cause: None, - status: SomethingWentWrong + status: SomethingWentWrong, })?; let hashed_value = string_based_b64_hash((&formatted_value).to_string()).to_string(); @@ -71,67 +66,86 @@ pub async fn add_new_override(state: &Data, override_value: Value, ret }) .await { - Ok(Ok(result)) => Ok(OverrideIdResponse {id: result.key}), - Ok(Err(err)) => + Ok(Ok(result)) => Ok(OverrideIdResponse { id: result.key }), + Ok(Err(err)) => { if return_if_present { - Ok(OverrideIdResponse {id: hashed_value}) + Ok(OverrideIdResponse { id: hashed_value }) } else { Err(AppError { message: Some("Data already exists".to_string()), cause: Some(Left(err.to_string())), - status: DataExists + status: DataExists, }) - }, - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) + } + } + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, + }), } } #[post("")] -pub async fn post_override(state: Data, body: Json) -> Result, AppError> { +pub async fn post_override( + state: Data, + body: Json, +) -> Result, AppError> { let override_value = body.clone(); Ok(Json(add_new_override(&state, override_value, false).await?)) } -pub async fn get_override_helper(state: &Data, key: String) -> Result, AppError> { +pub async fn get_override_helper( + state: &Data, + key: String, +) -> Result, AppError> { let db: Addr = state.db.clone(); - match db - .send(FetchOverride {key}) - .await - { + match db.send(FetchOverride { key }).await { Ok(Ok(result)) => Ok(Json(result.value)), Ok(Err(err)) => Err(AppError { message: Some("Failed to fetch value for given override key".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } pub async fn get_all_overrides(state: &Data) -> Result, AppError> { let db: Addr = state.db.clone(); - match db - .send(FetchAllOverrides) - .await - { + match db.send(FetchAllOverrides).await { Ok(Ok(result)) => Ok(result), Ok(Err(err)) => Err(AppError { message: Some("Failed to fetch value for given override key".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } #[get("/{key}")] -pub async fn get_override(state: Data, key: Path) -> Result, AppError> { +pub async fn get_override( + state: Data, + key: Path, +) -> Result, AppError> { get_override_helper(&state, key.to_owned()).await } -pub async fn delete_override_helper(state: &Data, id: String) -> Result, AppError> { +pub async fn delete_override_helper( + state: &Data, + id: String, +) -> Result, AppError> { let db: Addr = state.db.clone(); match db @@ -144,13 +158,20 @@ pub async fn delete_override_helper(state: &Data, id: String) -> Resul Ok(Err(err)) => Err(AppError { message: Some("Data not found for override deletion".to_string()), cause: Some(Left(err.to_string())), - status: NotFound + status: NotFound, + }), + Err(err) => Err(AppError { + message: None, + cause: Some(Left(err.to_string())), + status: DBError, }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) } } #[delete("/{key}")] -pub async fn delete_override(state: Data, id: Path) -> Result, AppError> { +pub async fn delete_override( + state: Data, + id: Path, +) -> Result, AppError> { delete_override_helper(&state, id.to_string()).await -} \ No newline at end of file +} diff --git a/src/handlers/context_overrides.rs b/src/db/handlers/context_overrides.rs similarity index 62% rename from src/handlers/context_overrides.rs rename to src/db/handlers/context_overrides.rs index 2b14bb96c..d6b22b57b 100644 --- a/src/handlers/context_overrides.rs +++ b/src/db/handlers/context_overrides.rs @@ -1,25 +1,30 @@ use diesel::QueryResult; +use crate::db::models::{ + db_models::CtxOverrides, insertables::context_overrides::CtxOverrideInsertion, +}; use crate::db::utils::DbActor; -use crate::models::db_models::CtxOverrides; +use crate::db::messages::context_overrides::{ + CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides, +}; use crate::db::schema::ctxoverrides::dsl::*; -use crate::messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::context_overrides::CtxOverrideInsertion; - impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: CreateCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for creating context override"); diesel::insert_into(ctxoverrides) .values(CtxOverrideInsertion { context_id: msg.context_id, - override_id: msg.override_id + override_id: msg.override_id, }) .get_result::(&mut conn) } @@ -29,7 +34,10 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: FetchCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); ctxoverrides .filter(context_id.eq(msg.context_id)) @@ -41,7 +49,10 @@ impl Handler for DbActor { type Result = QueryResult>; fn handle(&mut self, _msg: FetchAllCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); ctxoverrides.get_results::(&mut conn) } } @@ -50,10 +61,13 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: DeleteCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); diesel::delete(ctxoverrides) .filter(context_id.eq(msg.context_id)) .get_result::(&mut conn) } -} \ No newline at end of file +} diff --git a/src/handlers/contexts.rs b/src/db/handlers/contexts.rs similarity index 65% rename from src/handlers/contexts.rs rename to src/db/handlers/contexts.rs index a206d5276..700c5ea6d 100644 --- a/src/handlers/contexts.rs +++ b/src/db/handlers/contexts.rs @@ -1,20 +1,21 @@ use diesel::QueryResult; +use crate::db::models::{db_models::Contexts, insertables::contexts::NewContext}; use crate::db::utils::DbActor; -use crate::models::db_models::Contexts; +use crate::db::messages::contexts::{CreateContext, DeleteContext, FetchContext}; use crate::db::schema::contexts::dsl::*; -use crate::messages::contexts::{CreateContext, DeleteContext, FetchContext}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::contexts::NewContext; - impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: CreateContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for creating override"); diesel::insert_into(contexts) .values(NewContext { @@ -29,7 +30,10 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: FetchContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching override"); contexts .filter(key.eq(msg.key)) @@ -41,7 +45,10 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: DeleteContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching override"); diesel::delete(contexts) .filter(key.eq(msg.key)) diff --git a/src/handlers/dimensions.rs b/src/db/handlers/dimensions.rs similarity index 53% rename from src/handlers/dimensions.rs rename to src/db/handlers/dimensions.rs index 3f491d003..11ec20997 100644 --- a/src/handlers/dimensions.rs +++ b/src/db/handlers/dimensions.rs @@ -1,50 +1,52 @@ -use crate::models::db_models::Dimension; +use crate::db::models::db_models::Dimension; use crate::db::utils::DbActor; +use crate::db::messages::dimensions::{CreateDimension, FetchDimension, FetchDimensions}; +use crate::db::models::insertables::dimensions::NewDimension; use crate::db::schema::dimensions::dsl::*; -use crate::messages::dimensions::{ - FetchDimensions, - FetchDimension, - CreateDimension -}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::dimensions::NewDimension; impl Handler for DbActor { - type Result = QueryResult>; fn handle(&mut self, _msg: FetchDimensions, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimensions: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Fetch Dimensions: Unable to establish connection"); dimensions.get_results::(&mut conn) } } - impl Handler for DbActor { - type Result = QueryResult; fn handle(&mut self, msg: FetchDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimension: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Fetch Dimension: Unable to establish connection"); - dimensions.filter(dimension.eq(msg.dimension)).get_result::(&mut conn) + dimensions + .filter(dimension.eq(msg.dimension)) + .get_result::(&mut conn) } } - impl Handler for DbActor { - type Result = QueryResult; fn handle(&mut self, msg: CreateDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Create Dimension: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Create Dimension: Unable to establish connection"); - let new_dimension = NewDimension { + let new_dimension = NewDimension { dimension: msg.dimension, - priority: msg.priority + priority: msg.priority, }; diesel::insert_into(dimensions) diff --git a/src/handlers/global_config.rs b/src/db/handlers/global_config.rs similarity index 54% rename from src/handlers/global_config.rs rename to src/db/handlers/global_config.rs index b08cecb9b..b086efdbc 100644 --- a/src/handlers/global_config.rs +++ b/src/db/handlers/global_config.rs @@ -1,50 +1,51 @@ -use crate::models::db_models::GlobalConfig; +use crate::db::models::{db_models::GlobalConfig, insertables::global_config::NewGlobalConfigKey}; use crate::db::utils::DbActor; +use crate::db::messages::global_config::{CreateGlobalKey, FetchConfigKey, FetchGlobalConfig}; use crate::db::schema::global_config::dsl::*; -use crate::messages::global_config::{ - FetchGlobalConfig, - FetchConfigKey, - CreateGlobalKey -}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::global_config::NewGlobalConfigKey; impl Handler for DbActor { - type Result = QueryResult>; fn handle(&mut self, _msg: FetchGlobalConfig, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch GlobalConfig: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Fetch GlobalConfig: Unable to establish connection"); global_config.get_results::(&mut conn) } } - impl Handler for DbActor { - type Result = QueryResult; fn handle(&mut self, msg: FetchConfigKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimension: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Fetch Dimension: Unable to establish connection"); - global_config.filter(key.eq(msg.key)).get_result::(&mut conn) + global_config + .filter(key.eq(msg.key)) + .get_result::(&mut conn) } } - impl Handler for DbActor { - type Result = QueryResult; fn handle(&mut self, msg: CreateGlobalKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Create Dimension: Unable to establish connection"); + let mut conn = self + .0 + .get() + .expect("Create Dimension: Unable to establish connection"); - let new_key = NewGlobalConfigKey { + let new_key = NewGlobalConfigKey { key: msg.key, - value: msg.value + value: msg.value, }; diesel::insert_into(global_config) diff --git a/src/handlers/mod.rs b/src/db/handlers/mod.rs similarity index 82% rename from src/handlers/mod.rs rename to src/db/handlers/mod.rs index f26d16026..7b9cd61da 100644 --- a/src/handlers/mod.rs +++ b/src/db/handlers/mod.rs @@ -1,6 +1,6 @@ +pub mod context_overrides; +pub mod contexts; pub mod dimensions; pub mod global_config; +pub mod new_contexts; pub mod overrides; -pub mod contexts; -pub mod context_overrides; -pub mod new_contexts; \ No newline at end of file diff --git a/src/handlers/new_contexts.rs b/src/db/handlers/new_contexts.rs similarity index 56% rename from src/handlers/new_contexts.rs rename to src/db/handlers/new_contexts.rs index d3d6e0d15..08005e8c8 100644 --- a/src/handlers/new_contexts.rs +++ b/src/db/handlers/new_contexts.rs @@ -1,25 +1,28 @@ use diesel::QueryResult; +use crate::db::models::{db_models::NewContexts, insertables::new_contexts::NewContextInsertion}; use crate::db::utils::DbActor; -use crate::models::db_models::NewContexts; +use crate::db::messages::new_contexts::{ + CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext, +}; use crate::db::schema::newcontexts::dsl::*; -use crate::messages::new_contexts::{CreateNewContext, FetchAllNewContexts, FetchNewContext, DeleteNewContext}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::new_contexts::NewContextInsertion; - impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: CreateNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for creating context override"); diesel::insert_into(newcontexts) .values(NewContextInsertion { - key : msg.key, - value : msg.value, + key: msg.key, + value: msg.value, column1: msg.column1, column2: msg.column2, column3: msg.column3, @@ -33,23 +36,26 @@ impl Handler for DbActor { type Result = QueryResult>; fn handle(&mut self, _msg: FetchNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); // ! TODO :: Move filter directly over here (DB scanning) newcontexts.get_results::(&mut conn) - // .filter( - - // (column1 - // .eq(msg.column1) - // .or(column1.is_null()) - // ) - - // .and( - // column2 - // .eq(msg.column2) - // .or(column2.is_null()) - // ) - // ) + // .filter( + + // (column1 + // .eq(msg.column1) + // .or(column1.is_null()) + // ) + + // .and( + // column2 + // .eq(msg.column2) + // .or(column2.is_null()) + // ) + // ) } } @@ -57,7 +63,10 @@ impl Handler for DbActor { type Result = QueryResult>; fn handle(&mut self, _msg: FetchAllNewContexts, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); newcontexts.get_results::(&mut conn) } } @@ -66,10 +75,13 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: DeleteNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching context override"); diesel::delete(newcontexts) .filter(key.eq(msg.key)) .get_result::(&mut conn) } -} \ No newline at end of file +} diff --git a/src/handlers/overrides.rs b/src/db/handlers/overrides.rs similarity index 64% rename from src/handlers/overrides.rs rename to src/db/handlers/overrides.rs index d7b90fc74..28b5a3e27 100644 --- a/src/handlers/overrides.rs +++ b/src/db/handlers/overrides.rs @@ -1,20 +1,23 @@ use diesel::QueryResult; +use crate::db::models::{db_models::Overrides, insertables::overrides::NewOverride}; use crate::db::utils::DbActor; -use crate::models::db_models::Overrides; +use crate::db::messages::overrides::{ + CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride, +}; use crate::db::schema::overrides::dsl::*; -use crate::messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}; use actix::Handler; use diesel::{self, prelude::*}; -use crate::models::insertables::overrides::NewOverride; - impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: CreateOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for creating override"); diesel::insert_into(overrides) .values(NewOverride { @@ -29,7 +32,10 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: FetchOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching override"); overrides .filter(key.eq(msg.key)) @@ -41,7 +47,10 @@ impl Handler for DbActor { type Result = QueryResult>; fn handle(&mut self, _msg: FetchAllOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching override"); overrides.get_results::(&mut conn) } } @@ -50,7 +59,10 @@ impl Handler for DbActor { type Result = QueryResult; fn handle(&mut self, msg: DeleteOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); + let mut conn = self + .0 + .get() + .expect("Error on making DB connection for fetching override"); diesel::delete(overrides) .filter(key.eq(msg.key)) diff --git a/src/messages/context_overrides.rs b/src/db/messages/context_overrides.rs similarity index 55% rename from src/messages/context_overrides.rs rename to src/db/messages/context_overrides.rs index 02d480931..14a523ffd 100644 --- a/src/messages/context_overrides.rs +++ b/src/db/messages/context_overrides.rs @@ -1,27 +1,26 @@ - +use crate::db::models::db_models::CtxOverrides; use actix::Message; -use crate::models::db_models::CtxOverrides; use diesel::QueryResult; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct CreateCtxOverrides { pub context_id: String, - pub override_id: String + pub override_id: String, } #[derive(Message)] -#[rtype(result="QueryResult>")] +#[rtype(result = "QueryResult>")] pub struct FetchAllCtxOverrides; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct FetchCtxOverrides { pub context_id: String, } #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct DeleteCtxOverrides { pub context_id: String, -} \ No newline at end of file +} diff --git a/src/messages/contexts.rs b/src/db/messages/contexts.rs similarity index 61% rename from src/messages/contexts.rs rename to src/db/messages/contexts.rs index bd3c2b038..e54e404ad 100644 --- a/src/messages/contexts.rs +++ b/src/db/messages/contexts.rs @@ -1,23 +1,22 @@ - +use crate::db::models::db_models::Contexts; use actix::Message; -use crate::models::db_models::Contexts; use diesel::QueryResult; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct CreateContext { pub key: String, pub value: String, } #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct FetchContext { pub key: String, } #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct DeleteContext { pub key: String, } diff --git a/src/messages/dimensions.rs b/src/db/messages/dimensions.rs similarity index 79% rename from src/messages/dimensions.rs rename to src/db/messages/dimensions.rs index 2c4f9614a..5d62f21ed 100644 --- a/src/messages/dimensions.rs +++ b/src/db/messages/dimensions.rs @@ -1,4 +1,4 @@ -use crate::models::db_models::Dimension; +use crate::db::models::db_models::Dimension; use actix::Message; use diesel::QueryResult; @@ -6,16 +6,15 @@ use diesel::QueryResult; #[rtype(result = "QueryResult>")] pub struct FetchDimensions; - #[derive(Message)] #[rtype(result = "QueryResult")] pub struct FetchDimension { - pub dimension: String + pub dimension: String, } #[derive(Message)] #[rtype(result = "QueryResult")] pub struct CreateDimension { pub dimension: String, - pub priority: i32 + pub priority: i32, } diff --git a/src/messages/global_config.rs b/src/db/messages/global_config.rs similarity index 80% rename from src/messages/global_config.rs rename to src/db/messages/global_config.rs index b5f3beb38..ea4d1d406 100644 --- a/src/messages/global_config.rs +++ b/src/db/messages/global_config.rs @@ -1,4 +1,4 @@ -use crate::models::db_models::GlobalConfig; +use crate::db::models::db_models::GlobalConfig; use actix::Message; use diesel::QueryResult; use serde_json::Value; @@ -7,16 +7,15 @@ use serde_json::Value; #[rtype(result = "QueryResult>")] pub struct FetchGlobalConfig; - #[derive(Message)] #[rtype(result = "QueryResult")] pub struct FetchConfigKey { - pub key: String + pub key: String, } #[derive(Message)] #[rtype(result = "QueryResult")] pub struct CreateGlobalKey { pub key: String, - pub value: Value + pub value: Value, } diff --git a/src/messages/mod.rs b/src/db/messages/mod.rs similarity index 82% rename from src/messages/mod.rs rename to src/db/messages/mod.rs index f26d16026..7b9cd61da 100644 --- a/src/messages/mod.rs +++ b/src/db/messages/mod.rs @@ -1,6 +1,6 @@ +pub mod context_overrides; +pub mod contexts; pub mod dimensions; pub mod global_config; +pub mod new_contexts; pub mod overrides; -pub mod contexts; -pub mod context_overrides; -pub mod new_contexts; \ No newline at end of file diff --git a/src/messages/new_contexts.rs b/src/db/messages/new_contexts.rs similarity index 71% rename from src/messages/new_contexts.rs rename to src/db/messages/new_contexts.rs index f25375a8a..dc897bf7c 100644 --- a/src/messages/new_contexts.rs +++ b/src/db/messages/new_contexts.rs @@ -1,11 +1,10 @@ - +use crate::db::models::db_models::NewContexts; use actix::Message; -use serde_json::Value; -use crate::models::db_models::NewContexts; use diesel::QueryResult; +use serde_json::Value; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct CreateNewContext { pub key: String, pub value: Value, @@ -15,13 +14,12 @@ pub struct CreateNewContext { pub column4: Option, } - #[derive(Message)] -#[rtype(result="QueryResult>")] +#[rtype(result = "QueryResult>")] pub struct FetchAllNewContexts; #[derive(Message)] -#[rtype(result="QueryResult>")] +#[rtype(result = "QueryResult>")] pub struct FetchNewContext { pub column1: Option, pub column2: Option, @@ -30,7 +28,7 @@ pub struct FetchNewContext { } #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct DeleteNewContext { pub key: String, } diff --git a/src/messages/overrides.rs b/src/db/messages/overrides.rs similarity index 60% rename from src/messages/overrides.rs rename to src/db/messages/overrides.rs index a5adec965..cfe5b13e3 100644 --- a/src/messages/overrides.rs +++ b/src/db/messages/overrides.rs @@ -1,28 +1,27 @@ - +use crate::db::models::db_models::Overrides; use actix::Message; -use crate::models::db_models::Overrides; use diesel::QueryResult; use serde_json::Value; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct CreateOverride { pub key: String, pub value: Value, } #[derive(Message)] -#[rtype(result="QueryResult>")] +#[rtype(result = "QueryResult>")] pub struct FetchAllOverrides; #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct FetchOverride { pub key: String, } #[derive(Message)] -#[rtype(result="QueryResult")] +#[rtype(result = "QueryResult")] pub struct DeleteOverride { pub key: String, } diff --git a/src/db/mod.rs b/src/db/mod.rs index 0b41f16a7..81be08b94 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,5 @@ -pub mod utils; +pub mod handlers; +pub mod messages; +pub mod models; pub mod schema; +pub mod utils; diff --git a/src/models/db_models.rs b/src/db/models/db_models.rs similarity index 95% rename from src/models/db_models.rs rename to src/db/models/db_models.rs index 58f8622a4..ef662aac7 100644 --- a/src/models/db_models.rs +++ b/src/db/models/db_models.rs @@ -1,11 +1,9 @@ - - -use chrono::DateTime; +use crate::db::schema::{contexts, ctxoverrides, dimensions, global_config, overrides}; use chrono::offset::Utc; +use chrono::DateTime; use diesel::{Identifiable, Queryable}; use serde::Serialize; use serde_json::Value; -use crate::db::schema::{ctxoverrides,contexts, dimensions, global_config, overrides}; #[derive(Queryable, Debug, Identifiable, Serialize)] #[diesel(table_name = dimensions)] @@ -57,7 +55,6 @@ pub struct CtxOverrides { pub created_on: DateTime, } - #[derive(Queryable, Debug, Identifiable, Serialize)] #[diesel(table_name = contexts)] #[diesel(primary_key(key))] @@ -70,4 +67,4 @@ pub struct NewContexts { pub column4: Option, pub last_modified: DateTime, pub created_on: DateTime, -} \ No newline at end of file +} diff --git a/src/models/insertables/context_overrides.rs b/src/db/models/insertables/context_overrides.rs similarity index 76% rename from src/models/insertables/context_overrides.rs rename to src/db/models/insertables/context_overrides.rs index 1f5252cc5..634fef063 100644 --- a/src/models/insertables/context_overrides.rs +++ b/src/db/models/insertables/context_overrides.rs @@ -6,6 +6,6 @@ use crate::db::schema::ctxoverrides; #[derive(Debug, Insertable, Serialize)] #[diesel(table_name = ctxoverrides)] pub struct CtxOverrideInsertion { - pub context_id : String, - pub override_id : String -} \ No newline at end of file + pub context_id: String, + pub override_id: String, +} diff --git a/src/models/insertables/contexts.rs b/src/db/models/insertables/contexts.rs similarity index 79% rename from src/models/insertables/contexts.rs rename to src/db/models/insertables/contexts.rs index 5653807a7..7cab7238c 100644 --- a/src/models/insertables/contexts.rs +++ b/src/db/models/insertables/contexts.rs @@ -6,6 +6,6 @@ use crate::db::schema::contexts; #[derive(Debug, Insertable, Serialize)] #[diesel(table_name = contexts)] pub struct NewContext { - pub key : String, - pub value : String, + pub key: String, + pub value: String, } diff --git a/src/models/insertables/dimensions.rs b/src/db/models/insertables/dimensions.rs similarity index 100% rename from src/models/insertables/dimensions.rs rename to src/db/models/insertables/dimensions.rs diff --git a/src/models/insertables/global_config.rs b/src/db/models/insertables/global_config.rs similarity index 100% rename from src/models/insertables/global_config.rs rename to src/db/models/insertables/global_config.rs diff --git a/src/models/insertables/mod.rs b/src/db/models/insertables/mod.rs similarity index 82% rename from src/models/insertables/mod.rs rename to src/db/models/insertables/mod.rs index f26d16026..7b9cd61da 100644 --- a/src/models/insertables/mod.rs +++ b/src/db/models/insertables/mod.rs @@ -1,6 +1,6 @@ +pub mod context_overrides; +pub mod contexts; pub mod dimensions; pub mod global_config; +pub mod new_contexts; pub mod overrides; -pub mod contexts; -pub mod context_overrides; -pub mod new_contexts; \ No newline at end of file diff --git a/src/models/insertables/new_contexts.rs b/src/db/models/insertables/new_contexts.rs similarity index 94% rename from src/models/insertables/new_contexts.rs rename to src/db/models/insertables/new_contexts.rs index 1067dae34..ff40221f9 100644 --- a/src/models/insertables/new_contexts.rs +++ b/src/db/models/insertables/new_contexts.rs @@ -7,7 +7,7 @@ use crate::db::schema::newcontexts; #[derive(Debug, Insertable, Serialize)] #[diesel(table_name = newcontexts)] pub struct NewContextInsertion { - pub key : String, + pub key: String, pub value: Value, pub column1: Option, pub column2: Option, diff --git a/src/models/insertables/overrides.rs b/src/db/models/insertables/overrides.rs similarity index 82% rename from src/models/insertables/overrides.rs rename to src/db/models/insertables/overrides.rs index cd5330ab7..e216806bc 100644 --- a/src/models/insertables/overrides.rs +++ b/src/db/models/insertables/overrides.rs @@ -7,6 +7,6 @@ use crate::db::schema::overrides; #[derive(Debug, Insertable, Serialize)] #[diesel(table_name = overrides)] pub struct NewOverride { - pub key : String, - pub value : Value, + pub key: String, + pub value: Value, } diff --git a/src/models/mod.rs b/src/db/models/mod.rs similarity index 100% rename from src/models/mod.rs rename to src/db/models/mod.rs diff --git a/src/db/utils.rs b/src/db/utils.rs index 1de5ba484..1543f9c8c 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -1,11 +1,11 @@ use actix::{Actor, Addr, SyncContext}; use diesel::{ + r2d2::{ConnectionManager, Pool}, PgConnection, - r2d2::{ConnectionManager, Pool} }; pub struct AppState { - pub db: Addr + pub db: Addr, } pub struct DbActor(pub Pool>); @@ -16,6 +16,7 @@ impl Actor for DbActor { pub fn get_pool(db_url: &str) -> Pool> { let manager: ConnectionManager = ConnectionManager::::new(db_url); - Pool::builder().build(manager).expect("Error building a connection pool") + Pool::builder() + .build(manager) + .expect("Error building a connection pool") } - diff --git a/src/main.rs b/src/main.rs index 4b2b9090a..f4401cae2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,19 @@ mod api; -mod models; mod db; -mod messages; -mod handlers; mod utils; use api::primary::{ - global_config::{ - get_global_config_key, - get_global_config, - post_config_key_value, - }, - - dimensions::{ - get_dimensions, - get_dimension_key, - post_dimension - }, - overrides::{ - post_override, - delete_override, - get_override, - }, - contexts::{ - post_context, - delete_context, - get_context - }, - context_overrides::{ - post_ctx_override, - delete_ctx_override, - get_ctx_override, - }, - new_contexts::{ - delete_new_context, - get_new_context, - post_new_context, - }, + context_overrides::{delete_ctx_override, get_ctx_override, post_ctx_override}, + contexts::{delete_context, get_context, post_context}, + dimensions::{get_dimension_key, get_dimensions, post_dimension}, + global_config::{get_global_config, get_global_config_key, post_config_key_value}, + new_contexts::{delete_new_context, get_new_context, post_new_context}, + overrides::{delete_override, get_override, post_override}, }; use api::derived::{ - config::get_config, - context_override::add_new_context_override, - reduce::reduce_contexts_overrides, - promote::promote_contexts_overrides, + config::get_config, context_override::add_new_context_override, + promote::promote_contexts_overrides, reduce::reduce_contexts_overrides, }; // use crate::utils::validations::just_for_test; @@ -52,10 +22,9 @@ use dotenv; use std::env; use std::io::Result; -use db::utils::{get_pool, AppState, DbActor}; use actix::SyncArbiter; -use actix_web::{HttpServer, App, web::scope, middleware::Logger,web::Data}; - +use actix_web::{middleware::Logger, web::scope, web::Data, App, HttpServer}; +use db::utils::{get_pool, AppState, DbActor}; #[actix_web::main] async fn main() -> Result<()> { @@ -70,64 +39,52 @@ async fn main() -> Result<()> { HttpServer::new(move || { let logger: Logger = Logger::default(); App::new() - .app_data(Data::new(AppState {db: db_addr.clone()})) - .wrap(logger) - -/***************************** Primary api routes *****************************/ - .service( - scope("/global_config") - .service(get_global_config) - .service(get_global_config_key) - .service(post_config_key_value) - ) - .service( - scope("/dimensions") - .service(get_dimensions) - .service(get_dimension_key) - .service(post_dimension) - ) - .service( - scope("/context_overrides") - .service(post_ctx_override) - .service(delete_ctx_override) - .service(get_ctx_override) - ) - .service( - scope("/override") - .service(post_override) - .service(delete_override) - .service(get_override) - ).service( - scope("/oldcontext") - .service(post_context) - .service(delete_context) - .service(get_context) - - ).service( - scope("/context") - .service(get_new_context) - .service(post_new_context) - .service(delete_new_context) - - ) - -/***************************** Derived api routes *****************************/ - .service( - scope("/config") - .service(get_config) - ) - .service( - scope("add_context_overrides") - .service(add_new_context_override) - ) - .service( - scope("reduce") - .service(reduce_contexts_overrides) - ) - .service( - scope("promote") - .service(promote_contexts_overrides) - ) + .app_data(Data::new(AppState { + db: db_addr.clone(), + })) + .wrap(logger) + /***************************** Primary api routes *****************************/ + .service( + scope("/global_config") + .service(get_global_config) + .service(get_global_config_key) + .service(post_config_key_value), + ) + .service( + scope("/dimensions") + .service(get_dimensions) + .service(get_dimension_key) + .service(post_dimension), + ) + .service( + scope("/context_overrides") + .service(post_ctx_override) + .service(delete_ctx_override) + .service(get_ctx_override), + ) + .service( + scope("/override") + .service(post_override) + .service(delete_override) + .service(get_override), + ) + .service( + scope("/oldcontext") + .service(post_context) + .service(delete_context) + .service(get_context), + ) + .service( + scope("/context") + .service(get_new_context) + .service(post_new_context) + .service(delete_new_context), + ) + /***************************** Derived api routes *****************************/ + .service(scope("/config").service(get_config)) + .service(scope("add_context_overrides").service(add_new_context_override)) + .service(scope("reduce").service(reduce_contexts_overrides)) + .service(scope("promote").service(promote_contexts_overrides)) }) .bind(("localhost", 8080))? .workers(5) diff --git a/src/utils/errors.rs b/src/utils/errors.rs index d85318051..2f160e7f6 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -1,7 +1,12 @@ -use serde::Serialize; -use actix_web::{error::ResponseError, http::StatusCode, HttpResponse, Either::{Left, Right}}; use actix_web::Either; -use serde_json::{Value, to_value}; +use actix_web::{ + error::ResponseError, + http::StatusCode, + Either::{Left, Right}, + HttpResponse, +}; +use serde::Serialize; +use serde_json::{to_value, Value}; use std::fmt; #[derive(Debug, Clone, Serialize)] @@ -10,7 +15,7 @@ pub enum AppErrorType { DBError, NotFound, SomethingWentWrong, - BadRequest + BadRequest, } #[derive(Debug)] @@ -30,24 +35,27 @@ pub struct ErrorResponse { impl AppError { fn message(&self) -> String { match &*self { - AppError { message: Some(message), ..} => message.clone(), - _ => "Reason not available".to_string() + AppError { + message: Some(message), + .. + } => message.clone(), + _ => "Reason not available".to_string(), } } fn cause(&self) -> Option { match &*self { - AppError {cause, ..} => match cause { + AppError { cause, .. } => match cause { Some(Left(val)) => to_value(val.clone()).ok(), Some(Right(val)) => Some(val.clone()), _ => None, - } + }, } } } impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{:?}", self) } } @@ -66,13 +74,12 @@ impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { let status_code = self.status_code(); - let json_body = ErrorResponse - { message: self.message() - , cause: self.cause() - , status: self.status.clone() - }; + let json_body = ErrorResponse { + message: self.message(), + cause: self.cause(), + status: self.status.clone(), + }; HttpResponse::build(status_code).json(json_body) } } - diff --git a/src/utils/hash.rs b/src/utils/hash.rs index 2b41294cc..bcb5f2063 100644 --- a/src/utils/hash.rs +++ b/src/utils/hash.rs @@ -1,11 +1,13 @@ - use std::{ collections::hash_map::DefaultHasher, - hash::{Hash, Hasher} + hash::{Hash, Hasher}, }; -pub fn string_based_b64_hash(obj: T) -> u64 where T: Hash, { +pub fn string_based_b64_hash(obj: T) -> u64 +where + T: Hash, +{ let mut hasher = DefaultHasher::new(); obj.hash(&mut hasher); hasher.finish() -} \ No newline at end of file +} diff --git a/src/utils/helpers.rs b/src/utils/helpers.rs index 62876690d..951e542f3 100644 --- a/src/utils/helpers.rs +++ b/src/utils/helpers.rs @@ -1,35 +1,34 @@ -use serde_json::{Value, from_value, to_value}; -use std::{collections::BTreeMap}; +use serde_json::{from_value, to_value, Value}; +use std::collections::BTreeMap; pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { let b_tree: &BTreeMap = &from_value(json).ok()?; to_value(b_tree).ok() } -fn create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { +fn create_all_unique_subsets_helper(s: &Vec<&str>, idx: i32) -> Vec { let mut n = idx; - let mut vector_index = 0; - let mut result: Vec = Vec::new(); - - while n > 0 { - if (n & 1) == 1 { - result.push(s[vector_index].to_owned()); - } - n = n >> 1; + let mut vector_index = 0; + let mut result: Vec = Vec::new(); + + while n > 0 { + if (n & 1) == 1 { + result.push(s[vector_index].to_owned()); + } + n = n >> 1; vector_index += 1; - } - return result; + } + return result; } pub fn create_all_unique_subsets(s: &Vec<&str>) -> Vec> { - let mut res: Vec> = Vec::new(); + let mut res: Vec> = Vec::new(); for i in 1..(1 << s.len()) { - res.push(create_all_unique_subsets_helper(s ,i)); + res.push(create_all_unique_subsets_helper(s, i)); } - return res; + return res; } - pub fn split_stringified_key_value_pair(input: &str) -> Vec> { let conditions_vector_splits: Vec<&str> = input.split("&").collect(); let mut conditions_vector: Vec> = conditions_vector_splits @@ -42,12 +41,11 @@ pub fn split_stringified_key_value_pair(input: &str) -> Vec> { return conditions_vector; } -pub fn strip_double_quotes (str: &str) -> &str { - +pub fn strip_double_quotes(str: &str) -> &str { if str.starts_with("\"") && str.ends_with("\"") { let len = str.len(); - return &str[1..len-1]; + return &str[1..len - 1]; } str -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f2991c6d9..b099e3361 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,4 @@ +pub mod errors; pub mod hash; pub mod helpers; pub mod validations; -pub mod errors; diff --git a/src/utils/validations.rs b/src/utils/validations.rs index 809f29cda..eadb4e0e6 100644 --- a/src/utils/validations.rs +++ b/src/utils/validations.rs @@ -1,16 +1,16 @@ use std::collections::HashMap; -use serde_json::{Value, to_value}; +use serde_json::{to_value, Value}; fn are_both_of_same_type(a: &Value, b: &Value) -> bool { (a.is_boolean() && b.is_boolean()) - || (a.is_number() && b.is_number()) - || (a.is_string() && b.is_string()) - || (a.is_array() && b.is_array()) - || (a.is_object() && b.is_object()) + || (a.is_number() && b.is_number()) + || (a.is_string() && b.is_string()) + || (a.is_array() && b.is_array()) + || (a.is_object() && b.is_object()) } -fn type_of(a: &Value) -> String{ +fn type_of(a: &Value) -> String { if a.is_boolean() { return "Boolean".to_string(); } @@ -34,8 +34,11 @@ fn type_of(a: &Value) -> String{ return "Object".to_string(); } -fn create_type_mismatch_error(default_value: &Value, overriding_value: &Value, path: &mut Vec) -> Value { - +fn create_type_mismatch_error( + default_value: &Value, + overriding_value: &Value, + path: &mut Vec, +) -> Value { let mut error_map = HashMap::new(); error_map.insert("Path".to_string(), path.join("/")); error_map.insert("Expected type".to_string(), type_of(default_value)); @@ -47,12 +50,14 @@ fn create_type_mismatch_error(default_value: &Value, overriding_value: &Value, p } fn create_structure_mismatch_error(path: &mut Vec) -> Value { - let mut error_map = HashMap::new(); error_map.insert("Path".to_string(), path.join("/")); if let Some(_) = path.last() { - error_map.insert("Reason".to_string(), "Key not found in default config".to_string()); + error_map.insert( + "Reason".to_string(), + "Key not found in default config".to_string(), + ); } let error_message = to_value(error_map).unwrap(); // .map_err(|_| ValidationErrors::ErrorMessageParsingError)?; @@ -60,8 +65,11 @@ fn create_structure_mismatch_error(path: &mut Vec) -> Value { error_message } -fn validate_sub_tree_helper(default_tree: &Value, overriding_tree: &Value, path: &mut Vec) -> Result { - +fn validate_sub_tree_helper( + default_tree: &Value, + overriding_tree: &Value, + path: &mut Vec, +) -> Result { if !are_both_of_same_type(default_tree, overriding_tree) { let error_message = create_type_mismatch_error(default_tree, overriding_tree, path); return Err(error_message); @@ -71,18 +79,16 @@ fn validate_sub_tree_helper(default_tree: &Value, overriding_tree: &Value, path: return Ok(true); } - let subtree = - overriding_tree.as_object().unwrap(); + let subtree = overriding_tree.as_object().unwrap(); for (key, overriden_value) in (&*subtree).iter() { - let default_value = default_tree.get(key); path.push((&key).to_string()); let result = match default_value { Some(val) => validate_sub_tree_helper(val, overriden_value, path), - None => Err(create_structure_mismatch_error(path)) + None => Err(create_structure_mismatch_error(path)), }?; if !result { @@ -93,12 +99,10 @@ fn validate_sub_tree_helper(default_tree: &Value, overriding_tree: &Value, path: return Ok(true); } - pub fn validate_sub_tree(default_tree: &Value, overriding_tree: &Value) -> Result { - validate_sub_tree_helper(default_tree, overriding_tree,&mut vec!["".to_string()]) + validate_sub_tree_helper(default_tree, overriding_tree, &mut vec!["".to_string()]) } - // pub fn just_for_test() { // let default_json: Value = from_str(r#" From e07db3ce2620a83ae57893394119f51f2b7627d2 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 23 May 2023 11:22:04 +0530 Subject: [PATCH 012/352] refactor: removed old contexts table as the new table finalised over confluence would be name contexts --- src/api/derived/config.rs | 77 +------------- src/api/primary/contexts.rs | 197 ------------------------------------ src/api/primary/mod.rs | 1 - src/db/handlers/contexts.rs | 57 ----------- src/db/handlers/mod.rs | 1 - src/db/messages/contexts.rs | 22 ---- src/db/messages/mod.rs | 1 - src/db/models/db_models.rs | 10 -- src/main.rs | 7 -- src/utils/helpers.rs | 23 ----- 10 files changed, 3 insertions(+), 393 deletions(-) delete mode 100644 src/api/primary/contexts.rs delete mode 100644 src/db/handlers/contexts.rs delete mode 100644 src/db/messages/contexts.rs diff --git a/src/api/derived/config.rs b/src/api/derived/config.rs index f9c289b65..fbc648b3c 100644 --- a/src/api/derived/config.rs +++ b/src/api/derived/config.rs @@ -11,7 +11,7 @@ use actix_web::{ use serde_json::{to_value, Error, Value}; use crate::api::primary::{ - context_overrides::fetch_override_from_ctx_id, contexts::fetch_context, + context_overrides::fetch_override_from_ctx_id, global_config::get_complete_config, new_contexts::fetch_new_contexts, overrides::get_override_helper, }; @@ -21,9 +21,9 @@ use crate::utils::{ AppError, AppErrorType::{DBError, SomethingWentWrong}, }, - hash::string_based_b64_hash, - helpers::{create_all_unique_subsets, split_stringified_key_value_pair, strip_double_quotes}, }; +use crate::utils::helpers::strip_double_quotes; + use crate::AppState; @@ -35,77 +35,6 @@ fn default_parsing_error(err: Error) -> AppError { } } -async fn _get_context_overrides_object( - state: &Data, - query_string: &str, -) -> Result { - if query_string == "" { - return Ok(Value::default()); - } - - /************************************************************************************************************/ - // ! Optimize this section - let conditions_vector_temp: Vec = split_stringified_key_value_pair(&query_string) - .iter() - .map(|x| x.join("=")) - .collect(); - - let conditions_vector: Vec<&str> = conditions_vector_temp.iter().map(|s| &**s).collect(); - - /************************************************************************************************************/ - - let keys = create_all_unique_subsets(&conditions_vector); - - let mut override_map = HashMap::new(); - let mut contexts = Vec::new(); - - for item in keys { - // TODO :: Sort query based on key and fetch from DB - // Add the same logic while posting new context - - let key_string = item.to_owned().join("&"); - let hashed_key = string_based_b64_hash(&key_string).to_string(); - - if let Ok(override_id) = fetch_override_from_ctx_id(&state, &hashed_key).await { - let fetched_override_value = - get_override_helper(&state, override_id.to_owned()).await?; - - override_map.insert(override_id.to_owned(), fetched_override_value); - - contexts.push( - to_value(HashMap::from([ - ( - "overrideWithKeys", - to_value(override_id).map_err(default_parsing_error)?, - ), - ("condition", fetch_context(&state, &hashed_key).await?), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?, - ); - } - } - - to_value(HashMap::from([ - ( - "context", - to_value(contexts).map_err(default_parsing_error)?, - ), - ( - "overrides", - to_value(override_map).map_err(default_parsing_error)?, - ), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }) -} - async fn get_new_context_overrides_object( state: &Data, query_string: &str, diff --git a/src/api/primary/contexts.rs b/src/api/primary/contexts.rs deleted file mode 100644 index adb8cb4b7..000000000 --- a/src/api/primary/contexts.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; - -use actix::Addr; -use actix_web::{ - delete, get, post, - web::{Data, Json, Path}, - Either::Left, -}; -use serde::Serialize; -use serde_json::{from_value, to_value, Error, Value}; - -use crate::{ - db::messages::contexts::{CreateContext, DeleteContext, FetchContext}, - AppState, DbActor, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, - }, - hash::string_based_b64_hash, - helpers::split_stringified_key_value_pair, -}; - -#[derive(Serialize, Clone)] -pub struct ContextIdResponse { - pub id: String, -} - -fn default_parsing_error(err: Error) -> AppError { - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - } -} - -// TODO :: Implement Range based transforms -fn transform_context(raw_context_value: Value) -> Result { - // BTreeMap is used to make keys in sorted order - let b_tree: BTreeMap = - from_value(raw_context_value).map_err(default_parsing_error)?; - - let mut result: Vec = Vec::new(); - - for (key, value) in b_tree { - let value_object: HashMap = - from_value(value).map_err(default_parsing_error)?; - - let operator = value_object.get("operator").map(|val| val.to_string()); - let value = value_object.get("value").map(|val| val.to_string()); - - let max_range = value_object.get("max_range").map(|val| val.to_string()); - let min_range = value_object.get("min_range").map(|val| val.to_string()); - - match (operator.as_deref(), value, max_range, min_range) { - // ? `==` or `equals` ? - (Some("=="), Some(val), _, _) => result.push(key + "=" + &val), - - // TODO :: Implement Range based transforms properly - // ? Do we need to add inclusive check - (Some("range"), _, Some(max_range_val), Some(min_range_val)) => { - result.push(min_range_val + "<" + &key + "<" + &max_range_val) - } - (_, _, _, _) => (), - }; - } - - Ok(result.join("&")) -} - -pub async fn add_new_context( - state: &Data, - context_value: Value, -) -> Result { - let db: Addr = state.db.clone(); - - let transformed_context_value = transform_context(context_value)?; - - // ? TODO :: Post as an array of value - // ? TODO :: Sort query based on key and add to DB - let hashed_value = string_based_b64_hash(&transformed_context_value).to_string(); - - match db - .send(CreateContext { - key: hashed_value, - value: transformed_context_value, - }) - .await - { - Ok(Ok(result)) => Ok(ContextIdResponse { id: result.key }), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to add context".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -fn format_context_json(input: &str) -> Result { - let conditions_vector = split_stringified_key_value_pair(input); - - let mut formatted_conditions_vector = Vec::new(); - - for condition in conditions_vector { - let var_map = - to_value(HashMap::from([("var", condition[0])])).map_err(default_parsing_error)?; - let value_as_value = to_value(condition[1]).map_err(default_parsing_error)?; - - let value_arr = vec![[var_map, value_as_value]]; - - // Add range based queries - let condition_map = - to_value(HashMap::from([("==", value_arr)])).map_err(default_parsing_error)?; - - formatted_conditions_vector.push(condition_map); - } - - Ok(if formatted_conditions_vector.len() == 1 { - formatted_conditions_vector[0].to_owned() - } else { - to_value(HashMap::from([("and", formatted_conditions_vector)])) - .map_err(default_parsing_error)? - }) -} - -pub async fn fetch_context(state: &Data, key: &String) -> Result { - let db: Addr = state.db.clone(); - - let raw_context_string = match db - .send(FetchContext { - key: key.to_owned(), - }) - .await - { - Ok(Ok(result)) => Ok(result.value), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - }?; - - format_context_json(&raw_context_string) -} - -#[post("")] -pub async fn post_context( - state: Data, - body: Json, -) -> Result, AppError> { - let context_value = body.clone(); - Ok(Json(add_new_context(&state, context_value).await?)) -} - -#[get("/{key}")] -pub async fn get_context(state: Data, id: Path) -> Result, AppError> { - Ok(Json(fetch_context(&state, &id.to_string()).await?)) -} - -#[delete("/{key}")] -pub async fn delete_context( - state: Data, - key: Path, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(DeleteContext { - key: key.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to remove context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} diff --git a/src/api/primary/mod.rs b/src/api/primary/mod.rs index 7b9cd61da..5701bbda5 100644 --- a/src/api/primary/mod.rs +++ b/src/api/primary/mod.rs @@ -1,5 +1,4 @@ pub mod context_overrides; -pub mod contexts; pub mod dimensions; pub mod global_config; pub mod new_contexts; diff --git a/src/db/handlers/contexts.rs b/src/db/handlers/contexts.rs deleted file mode 100644 index 700c5ea6d..000000000 --- a/src/db/handlers/contexts.rs +++ /dev/null @@ -1,57 +0,0 @@ -use diesel::QueryResult; - -use crate::db::models::{db_models::Contexts, insertables::contexts::NewContext}; -use crate::db::utils::DbActor; - -use crate::db::messages::contexts::{CreateContext, DeleteContext, FetchContext}; -use crate::db::schema::contexts::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for creating override"); - - diesel::insert_into(contexts) - .values(NewContext { - key: msg.key, - value: msg.value, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching override"); - - contexts - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching override"); - - diesel::delete(contexts) - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} diff --git a/src/db/handlers/mod.rs b/src/db/handlers/mod.rs index 7b9cd61da..5701bbda5 100644 --- a/src/db/handlers/mod.rs +++ b/src/db/handlers/mod.rs @@ -1,5 +1,4 @@ pub mod context_overrides; -pub mod contexts; pub mod dimensions; pub mod global_config; pub mod new_contexts; diff --git a/src/db/messages/contexts.rs b/src/db/messages/contexts.rs deleted file mode 100644 index e54e404ad..000000000 --- a/src/db/messages/contexts.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::db::models::db_models::Contexts; -use actix::Message; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateContext { - pub key: String, - pub value: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchContext { - pub key: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct DeleteContext { - pub key: String, -} diff --git a/src/db/messages/mod.rs b/src/db/messages/mod.rs index 7b9cd61da..5701bbda5 100644 --- a/src/db/messages/mod.rs +++ b/src/db/messages/mod.rs @@ -1,5 +1,4 @@ pub mod context_overrides; -pub mod contexts; pub mod dimensions; pub mod global_config; pub mod new_contexts; diff --git a/src/db/models/db_models.rs b/src/db/models/db_models.rs index ef662aac7..7e0b78e21 100644 --- a/src/db/models/db_models.rs +++ b/src/db/models/db_models.rs @@ -35,16 +35,6 @@ pub struct Overrides { pub created_on: DateTime, } -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = contexts)] -#[diesel(primary_key(key))] -pub struct Contexts { - pub key: String, - pub value: String, - pub last_modified: DateTime, - pub created_on: DateTime, -} - #[derive(Queryable, Debug, Identifiable, Serialize)] #[diesel(table_name = ctxoverrides)] #[diesel(primary_key(context_id))] diff --git a/src/main.rs b/src/main.rs index f4401cae2..7116e2a94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ mod utils; use api::primary::{ context_overrides::{delete_ctx_override, get_ctx_override, post_ctx_override}, - contexts::{delete_context, get_context, post_context}, dimensions::{get_dimension_key, get_dimensions, post_dimension}, global_config::{get_global_config, get_global_config_key, post_config_key_value}, new_contexts::{delete_new_context, get_new_context, post_new_context}, @@ -68,12 +67,6 @@ async fn main() -> Result<()> { .service(delete_override) .service(get_override), ) - .service( - scope("/oldcontext") - .service(post_context) - .service(delete_context) - .service(get_context), - ) .service( scope("/context") .service(get_new_context) diff --git a/src/utils/helpers.rs b/src/utils/helpers.rs index 951e542f3..06b1b2b24 100644 --- a/src/utils/helpers.rs +++ b/src/utils/helpers.rs @@ -6,29 +6,6 @@ pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { to_value(b_tree).ok() } -fn create_all_unique_subsets_helper(s: &Vec<&str>, idx: i32) -> Vec { - let mut n = idx; - let mut vector_index = 0; - let mut result: Vec = Vec::new(); - - while n > 0 { - if (n & 1) == 1 { - result.push(s[vector_index].to_owned()); - } - n = n >> 1; - vector_index += 1; - } - return result; -} - -pub fn create_all_unique_subsets(s: &Vec<&str>) -> Vec> { - let mut res: Vec> = Vec::new(); - for i in 1..(1 << s.len()) { - res.push(create_all_unique_subsets_helper(s, i)); - } - return res; -} - pub fn split_stringified_key_value_pair(input: &str) -> Vec> { let conditions_vector_splits: Vec<&str> = input.split("&").collect(); let mut conditions_vector: Vec> = conditions_vector_splits From b04ac3fe6026030a42735d1b118bf29076f3dbd9 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 26 May 2023 21:56:57 +0530 Subject: [PATCH 013/352] feat: context/add api along with db setup --- Cargo.lock | 111 +++++++++++------- Cargo.toml | 3 +- diesel.toml | 7 +- docker-compose/postgres/db_init.sql | 52 -------- flake.nix | 1 + .../down.sql | 6 + .../up.sql | 36 ++++++ .../down.sql | 1 + .../2023-05-24-134019_create_contexts/up.sql | 7 ++ .../down.sql | 2 + .../2023-05-25-111645_create_overrides/up.sql | 6 + src/api/derived/config.rs | 14 +-- src/db/utils.rs | 2 + src/main.rs | 17 ++- src/v1/api/context/handlers.rs | 88 ++++++++++++++ src/v1/api/context/mod.rs | 3 + src/v1/api/context/types.rs | 14 +++ src/v1/api/mod.rs | 1 + src/v1/db/mod.rs | 2 + src/v1/db/models.rs | 26 ++++ src/v1/db/schema.rs | 22 ++++ src/v1/mod.rs | 2 + 22 files changed, 303 insertions(+), 120 deletions(-) create mode 100644 migrations/v1/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/v1/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/v1/2023-05-24-134019_create_contexts/down.sql create mode 100644 migrations/v1/2023-05-24-134019_create_contexts/up.sql create mode 100644 migrations/v1/2023-05-25-111645_create_overrides/down.sql create mode 100644 migrations/v1/2023-05-25-111645_create_overrides/up.sql create mode 100644 src/v1/api/context/handlers.rs create mode 100644 src/v1/api/context/mod.rs create mode 100644 src/v1/api/context/types.rs create mode 100644 src/v1/api/mod.rs create mode 100644 src/v1/db/mod.rs create mode 100644 src/v1/db/models.rs create mode 100644 src/v1/db/schema.rs create mode 100644 src/v1/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 376303ce1..5b4b7b31f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" dependencies = [ "actix-rt", "actix_derive", - "bitflags", + "bitflags 1.3.2", "bytes", "crossbeam-channel", "futures-core", @@ -32,7 +32,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-sink", @@ -55,7 +55,7 @@ dependencies = [ "actix-utils", "ahash 0.8.3", "base64", - "bitflags", + "bitflags 1.3.2", "brotli", "bytes", "bytestring", @@ -280,6 +280,18 @@ dependencies = [ "libc", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "atty" version = "0.2.14" @@ -309,6 +321,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" + +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -407,12 +439,19 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + [[package]] name = "context-aware-config" version = "0.1.0" dependencies = [ "actix", "actix-web", + "blake3", "chrono", "derive_more", "diesel", @@ -423,7 +462,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "uuid 0.8.2", + "uuid", ] [[package]] @@ -555,11 +594,11 @@ dependencies = [ [[package]] name = "diesel" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" +checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" dependencies = [ - "bitflags", + "bitflags 2.3.1", "byteorder", "chrono", "diesel_derives", @@ -567,19 +606,28 @@ dependencies = [ "pq-sys", "r2d2", "serde_json", - "uuid 1.3.2", + "uuid", ] [[package]] name = "diesel_derives" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad74fdcf086be3d4fdd142f67937678fe60ed431c3b2f08599e7687269410c4" +checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" dependencies = [ - "proc-macro-error", + "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.15", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.15", ] [[package]] @@ -590,6 +638,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1036,30 +1085,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.56" @@ -1125,7 +1150,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1300,6 +1325,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.109" @@ -1490,12 +1521,6 @@ dependencies = [ "serde", ] -[[package]] -name = "uuid" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 667765e13..fcadfa666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,5 @@ derive_more = "^0.99" # date and time chrono = { version = "0.4", features = ["serde"] } # ORM -diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid"] } +diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +blake3 = "1.3.3" diff --git a/diesel.toml b/diesel.toml index 88db6ba63..83a56ea8d 100644 --- a/diesel.toml +++ b/diesel.toml @@ -1,8 +1,5 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - [print_schema] -file = "src/db/schema.rs" +file = "src/v1/db/schema.rs" [migrations_directory] -dir = "migrations" +dir = "migrations/v1" diff --git a/docker-compose/postgres/db_init.sql b/docker-compose/postgres/db_init.sql index 8add3e8b9..e69de29bb 100644 --- a/docker-compose/postgres/db_init.sql +++ b/docker-compose/postgres/db_init.sql @@ -1,52 +0,0 @@ - -CREATE table dimensions ( - dimension VARCHAR NOT NULL, - priority integer NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(dimension) -); - -CREATE TABLE global_config ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE overrides ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE contexts ( - key VARCHAR NOT NULL, - value VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE ctxoverrides ( - context_id VARCHAR NOT NULL, - override_id VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(context_id) -); - -CREATE TABLE newcontexts ( - key VARCHAR NOT NULL, - value JSON NOT NULL, - column1 VARCHAR, - column2 VARCHAR, - column3 VARCHAR, - column4 VARCHAR, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); diff --git a/flake.nix b/flake.nix index 9343ffed1..abde5ee88 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,7 @@ rustfmt bacon cargo-watch + diesel-cli docker-compose ]; # buildInputs = with pkgs; [ ]; diff --git a/migrations/v1/00000000000000_diesel_initial_setup/down.sql b/migrations/v1/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 000000000..a9f526091 --- /dev/null +++ b/migrations/v1/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/v1/00000000000000_diesel_initial_setup/up.sql b/migrations/v1/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 000000000..d68895b1a --- /dev/null +++ b/migrations/v1/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/v1/2023-05-24-134019_create_contexts/down.sql b/migrations/v1/2023-05-24-134019_create_contexts/down.sql new file mode 100644 index 000000000..e72d46b4a --- /dev/null +++ b/migrations/v1/2023-05-24-134019_create_contexts/down.sql @@ -0,0 +1 @@ +DROP TABLE contexts; diff --git a/migrations/v1/2023-05-24-134019_create_contexts/up.sql b/migrations/v1/2023-05-24-134019_create_contexts/up.sql new file mode 100644 index 000000000..9aefc8902 --- /dev/null +++ b/migrations/v1/2023-05-24-134019_create_contexts/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE contexts ( + id VARCHAR PRIMARY KEY, + value JSON NOT NULL, + override_id VARCHAR NOT NULL, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR NOT NULL +); diff --git a/migrations/v1/2023-05-25-111645_create_overrides/down.sql b/migrations/v1/2023-05-25-111645_create_overrides/down.sql new file mode 100644 index 000000000..e9fb2eaf0 --- /dev/null +++ b/migrations/v1/2023-05-25-111645_create_overrides/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE overrides; diff --git a/migrations/v1/2023-05-25-111645_create_overrides/up.sql b/migrations/v1/2023-05-25-111645_create_overrides/up.sql new file mode 100644 index 000000000..a941cb729 --- /dev/null +++ b/migrations/v1/2023-05-25-111645_create_overrides/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE overrides ( + id VARCHAR PRIMARY KEY, + value JSON NOT NULL, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR NOT NULL +); diff --git a/src/api/derived/config.rs b/src/api/derived/config.rs index fbc648b3c..9691fce16 100644 --- a/src/api/derived/config.rs +++ b/src/api/derived/config.rs @@ -11,20 +11,16 @@ use actix_web::{ use serde_json::{to_value, Error, Value}; use crate::api::primary::{ - context_overrides::fetch_override_from_ctx_id, - global_config::get_complete_config, new_contexts::fetch_new_contexts, - overrides::get_override_helper, + context_overrides::fetch_override_from_ctx_id, global_config::get_complete_config, + new_contexts::fetch_new_contexts, overrides::get_override_helper, }; -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{DBError, SomethingWentWrong}, - }, +use crate::utils::errors::{ + AppError, + AppErrorType::{DBError, SomethingWentWrong}, }; use crate::utils::helpers::strip_double_quotes; - use crate::AppState; fn default_parsing_error(err: Error) -> AppError { diff --git a/src/db/utils.rs b/src/db/utils.rs index 1543f9c8c..f0dc13a64 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -4,8 +4,10 @@ use diesel::{ PgConnection, }; +//TODOP separate out AppState from DB pub struct AppState { pub db: Addr, + pub db_pool: Pool>, } pub struct DbActor(pub Pool>); diff --git a/src/main.rs b/src/main.rs index 7116e2a94..5865b448a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ mod api; mod db; mod utils; +mod v1; use api::primary::{ context_overrides::{delete_ctx_override, get_ctx_override, post_ctx_override}, dimensions::{get_dimension_key, get_dimensions, post_dimension}, global_config::{get_global_config, get_global_config_key, post_config_key_value}, - new_contexts::{delete_new_context, get_new_context, post_new_context}, overrides::{delete_override, get_override, post_override}, }; @@ -15,8 +15,6 @@ use api::derived::{ promote::promote_contexts_overrides, reduce::reduce_contexts_overrides, }; -// use crate::utils::validations::just_for_test; - use dotenv; use std::env; use std::io::Result; @@ -25,6 +23,8 @@ use actix::SyncArbiter; use actix_web::{middleware::Logger, web::scope, web::Data, App, HttpServer}; use db::utils::{get_pool, AppState, DbActor}; +use v1::api::*; + #[actix_web::main] async fn main() -> Result<()> { // just_for_test(); @@ -34,12 +34,14 @@ async fn main() -> Result<()> { env_logger::init(); let db_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set in environment"); let pool = get_pool(&db_url); - let db_addr = SyncArbiter::start(5, move || DbActor(pool.clone())); + let pool_cl = pool.clone(); + let db_addr = SyncArbiter::start(5, move || DbActor(pool_cl.clone())); HttpServer::new(move || { let logger: Logger = Logger::default(); App::new() .app_data(Data::new(AppState { db: db_addr.clone(), + db_pool: pool.clone(), })) .wrap(logger) /***************************** Primary api routes *****************************/ @@ -67,12 +69,7 @@ async fn main() -> Result<()> { .service(delete_override) .service(get_override), ) - .service( - scope("/context") - .service(get_new_context) - .service(post_new_context) - .service(delete_new_context), - ) + .service(scope("/context").service(context::endpoints())) /***************************** Derived api routes *****************************/ .service(scope("/config").service(get_config)) .service(scope("add_context_overrides").service(add_new_context_override)) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs new file mode 100644 index 000000000..98717540f --- /dev/null +++ b/src/v1/api/context/handlers.rs @@ -0,0 +1,88 @@ +use crate::{ + db::utils::AppState, + v1::{ + api::context::types::{AddContextReq, AddContextResp}, + db::{ + models::{Context, Override}, + schema::{contexts::dsl::contexts, overrides::dsl::overrides}, + }, + }, +}; +use actix_web::{ + put, + web::{self, Data}, + HttpResponse, Scope, +}; +use chrono::Utc; +use diesel::{ + result::{DatabaseErrorKind::*, Error::DatabaseError}, + RunQueryDsl, +}; +use serde_json::json; + +pub fn endpoints() -> Scope { + Scope::new("").service(add_contexts_overrides) +} + +#[put("add")] +async fn add_contexts_overrides( + req: web::Json, + state: Data, +) -> HttpResponse { + let ctxt_cond = json!({ + "and": req.context + }); + let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); + let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); + + let new_override = Override { + id: override_id.clone(), + value: req.r#override.clone(), + created_at: Utc::now(), + created_by: "some_user".to_string(), //TODO update once authentication is added + }; + + let new_ctxt = Context { + id: context_id.clone(), + value: ctxt_cond, + override_id: override_id.clone(), + created_at: Utc::now(), + created_by: "some_user".to_string(), + }; + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + let txn = conn.build_transaction().run(|conn| { + diesel::insert_into(overrides) + .values(&new_override) + .on_conflict_do_nothing() + .execute(conn)?; + diesel::insert_into(contexts) + .values(&new_ctxt) + .execute(conn) + }); + + let resp = AddContextResp { + context_id, + override_id, + }; + + match txn { + Ok(_) => HttpResponse::Created() + .insert_header(("x-info", "new context created")) + .json(resp), + Err(DatabaseError(UniqueViolation, _)) => HttpResponse::Ok() + .insert_header(("x-info", "context already exists")) + .json(resp), + e => { + println!("DB transaction failed with error: {e:?}"); + return HttpResponse::InternalServerError().finish(); + } + } +} diff --git a/src/v1/api/context/mod.rs b/src/v1/api/context/mod.rs new file mode 100644 index 000000000..ebe17b924 --- /dev/null +++ b/src/v1/api/context/mod.rs @@ -0,0 +1,3 @@ +mod handlers; +mod types; +pub use handlers::endpoints; diff --git a/src/v1/api/context/types.rs b/src/v1/api/context/types.rs new file mode 100644 index 000000000..c695e0a1f --- /dev/null +++ b/src/v1/api/context/types.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Deserialize)] +pub struct AddContextReq { + pub context: Vec, + pub r#override: Value, +} + +#[derive(Serialize)] +pub struct AddContextResp { + pub context_id: String, + pub override_id: String, +} diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs new file mode 100644 index 000000000..9efb2ab19 --- /dev/null +++ b/src/v1/api/mod.rs @@ -0,0 +1 @@ +pub mod context; diff --git a/src/v1/db/mod.rs b/src/v1/db/mod.rs new file mode 100644 index 000000000..d5cbad7e2 --- /dev/null +++ b/src/v1/db/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod schema; diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs new file mode 100644 index 000000000..21ef0311a --- /dev/null +++ b/src/v1/db/models.rs @@ -0,0 +1,26 @@ +use crate::v1::db::schema::*; +use chrono::offset::Utc; +use chrono::DateTime; +use diesel::{Insertable, Queryable, Selectable}; +use serde_json::Value; + +#[derive(Queryable, Selectable, Insertable)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(id))] +pub struct Context { + pub id: String, + pub value: Value, + pub override_id: String, + pub created_at: DateTime, + pub created_by: String, +} + +#[derive(Queryable, Selectable, Insertable)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(id))] +pub struct Override { + pub id: String, + pub value: Value, + pub created_at: DateTime, + pub created_by: String, +} diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs new file mode 100644 index 000000000..0fbc143a1 --- /dev/null +++ b/src/v1/db/schema.rs @@ -0,0 +1,22 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + } +} + +diesel::table! { + overrides (id) { + id -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + } +} + +diesel::allow_tables_to_appear_in_same_query!(contexts, overrides,); diff --git a/src/v1/mod.rs b/src/v1/mod.rs new file mode 100644 index 000000000..037782038 --- /dev/null +++ b/src/v1/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod db; From f8e49b012a63be7d0b4ccc4d7afe949f62bf122e Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Mon, 29 May 2023 18:24:06 +0530 Subject: [PATCH 014/352] feat: Added context fetch API - Exposed GET@/context/ --- src/v1/api/context/handlers.rs | 43 +++++++++++++++++++++++++++++++--- src/v1/db/models.rs | 3 ++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 98717540f..67bcf577f 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -9,19 +9,22 @@ use crate::{ }, }; use actix_web::{ - put, + put, get, web::{self, Data}, - HttpResponse, Scope, + HttpResponse, Scope, Result, Responder, error }; use chrono::Utc; use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, RunQueryDsl, + QueryDsl, ExpressionMethods, QueryResult }; use serde_json::json; pub fn endpoints() -> Scope { - Scope::new("").service(add_contexts_overrides) + Scope::new("") + .service(add_contexts_overrides) + .service(get_context) } #[put("add")] @@ -86,3 +89,37 @@ async fn add_contexts_overrides( } } } + +#[get("/{ctx_id}")] +async fn get_context( + path: web::Path, + state: Data +) -> Result { + use crate::v1::db::schema::contexts::dsl::*; + + let ctx_id = path.into_inner(); + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("Unable to get db connection from pool, error: {e}"); + return Err(error::ErrorInternalServerError("")); + } + }; + + let result: QueryResult> = contexts + .filter(id.eq(ctx_id)) + .load(&mut conn); + + let ctx_vec = match result { + Ok(ctx_vec) => ctx_vec, + Err(e) => { + println!("Failed to execute query, error: {e}"); + return Err(error::ErrorInternalServerError("")); + } + }; + + match ctx_vec.first() { + Some(ctx) => Ok(web::Json(ctx.clone())), + _ => Err(error::ErrorNotFound("")) + } +} diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 21ef0311a..6c8555a9c 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -2,9 +2,10 @@ use crate::v1::db::schema::*; use chrono::offset::Utc; use chrono::DateTime; use diesel::{Insertable, Queryable, Selectable}; +use serde::Serialize; use serde_json::Value; -#[derive(Queryable, Selectable, Insertable)] +#[derive(Queryable, Selectable, Insertable, Clone, Serialize)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(id))] pub struct Context { From b8e0b0d7104ba75e66ce2c01ae8c88869255dfc7 Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Wed, 31 May 2023 15:14:51 +0530 Subject: [PATCH 015/352] feat: Added health-check endpoint. --- src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5865b448a..c327a7fde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,17 +20,14 @@ use std::env; use std::io::Result; use actix::SyncArbiter; -use actix_web::{middleware::Logger, web::scope, web::Data, App, HttpServer}; +use actix_web::{middleware::Logger, web::scope, web::Data, App, HttpServer, web::get, HttpResponse}; use db::utils::{get_pool, AppState, DbActor}; use v1::api::*; #[actix_web::main] async fn main() -> Result<()> { - // just_for_test(); dotenv::dotenv().ok(); - std::env::set_var("RUST_LOG", "debug"); - std::env::set_var("RUST_BACKTRACE", "1"); env_logger::init(); let db_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set in environment"); let pool = get_pool(&db_url); @@ -44,6 +41,7 @@ async fn main() -> Result<()> { db_pool: pool.clone(), })) .wrap(logger) + .route("/health", get().to(|| async { HttpResponse::Ok().body("Health is good :D")})) /***************************** Primary api routes *****************************/ .service( scope("/global_config") From 7dd03a8bc2e54015ebef7708e5173aa85b7bc03a Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Tue, 6 Jun 2023 17:30:13 +0530 Subject: [PATCH 016/352] ci: Created pipeline for automated-deployment --- Jenkinsfile | 103 ++++++++++++++++++++++++++++++++++++++++++++++------ makefile | 19 +++++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8a56c430c..c7798454a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,36 +1,115 @@ - -def createDockerAndPush(version, packageVersion) { - sh('cd backend') - sh('make build -e packageVersion=0.0.2') +def getRegistryHost(aws_acc_id, region) { + return aws_acc_id + ".dkr.ecr." + region + ".amazonaws.com"; } pipeline { - agent { - label 'sdk' + agent { label 'hypersdk' } + environment { + SKIP_CI = false; + REGION = "ap-south-1"; + REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); + REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); + AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; } stages { stage('Checkout') { steps { - scmSkip(deleteBuild: false, skipPattern:'.*\\[skip ci\\].*') script { - if (sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", returnStatus: true) == 0) { - currentBuild.result = 'ABORTED' - error 'Aborting because commit message contains [skip ci]' + isSkipCI = sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", + returnStatus: true) + if (isSkipCI == 0) { + env.SKIP_CI = true; } + env.COMMIT_HASH = sh(returnStdout: true, script: "git rev-parse --short HEAD").trim() } } } + stage('Test') { + when { expression { SKIP_CI == 'false' } } + steps { sh 'make ci-test' } + } + stage('Build Image') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { sh 'make ci-build -e VERSION=${COMMIT_HASH}' } + } + + stage('Push Image To Sandbox Registry') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${COMMIT_HASH} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_SBX} + ''' + } + } + + stage('Push Image To Production Registry') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${COMMIT_HASH} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_PROD} + ''' + } + } + + stage('Create Integ Release Tracker') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + environment { + CREDS = credentials('AP_INTEG_ID') + COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") + CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; + AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") + } steps { - createDockerAndPush('prod', '0.0.2') + sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Basic ${CREDS_PSW}' \ + --data-raw '{ + "service": ["CONTEXT_AWARE_CONFIG"], + "release_manager": "${AUTHOR_NAME}", + "release_tag": "", + "new_version": "${COMMIT_HASH}", + "docker_image" : "${COMMIT_HASH}", + "priority" : 0, + "cluster" : "INTEG_CLUSTER", + "change_log": "${CHANGE_LOG}", + "rollout_strategy": [ + { + "rollout": 100, + "cooloff": 0, + "pods": 1 + } + ], + "description": "${CHANGE_LOG}", + "product": "HYPER_SDK", + "mode" : "AUTO", + "env" : "INTEG" + }'; + """ } } stage('Summary') { steps { script { - echo 'Successfully compiled' + echo 'Build Success' } } } diff --git a/makefile b/makefile index 8c798a52e..7ef1b13c2 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,23 @@ IMAGE_NAME ?= context-aware-config build: - docker build -f Dockerfile -t $(IMAGE_NAME):$(packageVersion) . + cargo build + +ci-test: +## Un-comment once agent has 'cargo' & 'libpq5'. + #cargo build + +ci-build: + docker build -t $(IMAGE_NAME):$(VERSION) . + +ci-push: registry-login + docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY_HOST)/$(IMAGE_NAME):$(VERSION) + docker push $(REGISTRY_HOST)/$(IMAGE_NAME):$(VERSION) + +registry-login: + aws ecr get-login-password --region $(REGION) | \ + docker login \ + --username AWS \ + --password-stdin $(REGISTRY_HOST) default: build From 8bf3a27915b0d1d2cbc653bfe42d63a1acf4d9e2 Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Wed, 7 Jun 2023 17:23:11 +0530 Subject: [PATCH 017/352] ci: Commented out prod docker push. --- Jenkinsfile | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c7798454a..23fd5b468 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -52,19 +52,20 @@ pipeline { } } - stage('Push Image To Production Registry') { - when { - expression { SKIP_CI == 'false' } - branch 'main' - } - steps { - sh '''make ci-push -e \ - VERSION=${COMMIT_HASH} \ - REGION=${REGION} \ - REGISTRY_HOST=${REGISTRY_HOST_PROD} - ''' - } - } + // Disabled for now, as prod setup is not complete. + //stage('Push Image To Production Registry') { + // when { + // expression { SKIP_CI == 'false' } + // branch 'main' + // } + // steps { + // sh '''make ci-push -e \ + // VERSION=${COMMIT_HASH} \ + // REGION=${REGION} \ + // REGISTRY_HOST=${REGISTRY_HOST_PROD} + // ''' + // } + //} stage('Create Integ Release Tracker') { when { From 18342c83e6695e101e6c531ba6a28fb3deffa83b Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 6 Jun 2023 21:09:09 +0530 Subject: [PATCH 018/352] feat: added localstack setup along with kms - generation DATABASE_URL from envs, kms cyphers - need to come back to kms localstack setup to debug localstack's invalid key issue --- .env.example | 10 + .gitignore | 6 +- Cargo.lock | 649 +++++++++++++++++- Cargo.toml | 4 + docker-compose.yaml | 17 +- docker-compose/localstack/Dockerfile | 9 + docker-compose/localstack/localstack_setup.sh | 19 + flake.nix | 2 + makefile | 10 + scripts/server-setup.sh | 2 + src/db/utils.rs | 18 +- src/main.rs | 4 +- src/v1/api/context/handlers.rs | 28 +- src/v1/aws/kms.rs | 48 ++ src/v1/aws/mod.rs | 1 + src/v1/db/schema.rs | 5 +- src/v1/helpers.rs | 14 + src/v1/mod.rs | 2 + 18 files changed, 818 insertions(+), 30 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose/localstack/Dockerfile create mode 100644 docker-compose/localstack/localstack_setup.sh create mode 100644 src/v1/aws/kms.rs create mode 100644 src/v1/aws/mod.rs create mode 100644 src/v1/helpers.rs diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..12780bedb --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +DATABASE_URL=postgres://postgres:docker@localhost:5432/config?sslmode=disable +RUST_LOG=info +AWS_ACCESS_KEY_ID=test +AWS_SECRET_ACCESS_KEY=test +AWS_SESSION_TOKEN=test +AWS_REGION=ap-south-1 +DB_USER=postgres +DB_HOST=localhost:5432 +DB_NAME=config +APP_ENV=DEV diff --git a/.gitignore b/.gitignore index 27cfebc72..39d37f871 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,8 @@ backend/.tmp backend/logfile backend/*PGSQL* backend/.env -.direnv \ No newline at end of file +.direnv + +# dev +bacon.toml +docker-compose/localstack/export_cyphers.sh diff --git a/Cargo.lock b/Cargo.lock index 5b4b7b31f..406df37ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64", + "base64 0.21.0", "bitflags 1.3.2", "brotli", "bytes", @@ -292,6 +292,17 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "atty" version = "0.2.14" @@ -309,6 +320,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -338,7 +355,16 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "digest", + "digest 0.10.6", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", ] [[package]] @@ -452,12 +478,16 @@ dependencies = [ "actix", "actix-web", "blake3", + "bytes", "chrono", "derive_more", "diesel", "dotenv", "env_logger", "log", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", "serde", "serde_json", "strum", @@ -482,6 +512,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -535,6 +575,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "cxx" version = "1.0.94" @@ -630,17 +680,47 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "crypto-common", "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -669,6 +749,36 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.26" @@ -685,6 +795,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -694,12 +819,65 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -718,10 +896,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -794,6 +978,28 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "http" version = "0.2.9" @@ -805,6 +1011,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -823,6 +1040,43 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.56" @@ -867,6 +1121,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" @@ -897,6 +1171,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.144" @@ -912,6 +1192,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "local-channel" version = "0.1.3" @@ -949,6 +1235,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.5.0" @@ -982,6 +1279,24 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1017,6 +1332,56 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1035,7 +1400,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -1153,6 +1518,26 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.8.1" @@ -1170,6 +1555,89 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "rusoto_core" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_kms" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "serde", + "serde_json", +] + +[[package]] +name = "rusoto_signature" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" +dependencies = [ + "base64 0.13.1", + "bytes", + "chrono", + "digest 0.9.0", + "futures", + "hex", + "hmac", + "http", + "hyper", + "log", + "md-5", + "percent-encoding", + "pin-project-lite", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "tokio", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1179,6 +1647,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -1191,6 +1673,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -1212,6 +1703,29 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.17" @@ -1269,9 +1783,28 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1353,6 +1886,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1362,6 +1909,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "time" version = "0.1.45" @@ -1425,13 +1992,36 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -1446,6 +2036,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -1467,6 +2063,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.16.0" @@ -1533,6 +2135,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1639,6 +2251,21 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -1771,6 +2398,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "xml-rs" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + [[package]] name = "zstd" version = "0.12.3+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index fcadfa666..187b8f861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,7 @@ chrono = { version = "0.4", features = ["serde"] } # ORM diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } blake3 = "1.3.3" +rusoto_kms = "0.48.0" +rusoto_signature = "0.48.0" +bytes = "1.4.0" +rusoto_core = "0.48.0" diff --git a/docker-compose.yaml b/docker-compose.yaml index b4ff67276..037ce9d1e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,10 +21,9 @@ services: ports: - "8080:8080" - # Middleware postgres: build: ./docker-compose/postgres/ - container_name: context-aware-config_postgres_instance + container_name: context-aware-config_postgres environment: POSTGRES_PASSWORD: "docker" POSTGRES_DB: "config" @@ -33,6 +32,20 @@ services: ports: - "5432:5432" + localstack: + build : ./docker-compose/localstack/ + container_name: context-aware-config_localstack_instance + ports: + - "4510-4559:4510-4559" # external service port range + - "4566:4566" # LocalStack Edge Proxy + - "4571:4571" + network_mode: bridge + environment: + LOCALSTACK_SERVICES: s3, sns, sqs, logs, cloudwatch, kms + AWS_DEFAULT_REGION: ap-south-1 + EDGE_PORT: 4566 + volumes: + - ./docker-compose/localstack/export_cyphers.sh:/etc/localstack/export_cyphers.sh networks: library-network: diff --git a/docker-compose/localstack/Dockerfile b/docker-compose/localstack/Dockerfile new file mode 100644 index 000000000..ed33381cf --- /dev/null +++ b/docker-compose/localstack/Dockerfile @@ -0,0 +1,9 @@ +FROM localstack/localstack:1.3.0 + +RUN aws configure set aws_access_key_id test +RUN aws configure set aws_secret_access_key test +RUN aws configure set default.region ap-south-1 +RUN apt-get install -y jq + +COPY ./localstack_setup.sh /etc/localstack/init/ready.d/localstack_setup.sh +RUN chmod +x /etc/localstack/init/ready.d/localstack_setup.sh diff --git a/docker-compose/localstack/localstack_setup.sh b/docker-compose/localstack/localstack_setup.sh new file mode 100644 index 000000000..617aba0e8 --- /dev/null +++ b/docker-compose/localstack/localstack_setup.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +mkdir /alloo +# CONSTANTS + +region="ap-south-1" +alias aws="aws --endpoint-url=http://localhost:4566 --region=${region}" + +# ****** KMS ******* + +echo "#!/bin/sh" > /etc/localstack/export_cyphers.sh +secret_key_id=`aws kms create-key | jq -r .KeyMetadata.KeyId` + +kms_encrypt(){ + cypher=`aws kms encrypt --key-id $secret_key_id --plaintext $2 | jq -r .CiphertextBlob` + echo "export $1=$cypher" >> /etc/localstack/export_cyphers.sh +} + +kms_encrypt DB_PASSWORD docker diff --git a/flake.nix b/flake.nix index abde5ee88..65a23d56c 100644 --- a/flake.nix +++ b/flake.nix @@ -38,6 +38,8 @@ cargo-watch diesel-cli docker-compose + stdenv.cc + darwin.apple_sdk.frameworks.Security ]; # buildInputs = with pkgs; [ ]; }; diff --git a/makefile b/makefile index 7ef1b13c2..d747c1467 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,7 @@ IMAGE_NAME ?= context-aware-config +SHELL := /bin/bash + build: cargo build @@ -20,4 +22,12 @@ registry-login: --username AWS \ --password-stdin $(REGISTRY_HOST) +run: + touch ./docker-compose/localstack/export_cyphers.sh + cargo build --color always + docker-compose up -d postgres localstack + pkill -f context-aware-config & + source ./docker-compose/localstack/export_cyphers.sh && \ + cargo run --color always + default: build diff --git a/scripts/server-setup.sh b/scripts/server-setup.sh index 1f9bd4646..e93b6dd36 100644 --- a/scripts/server-setup.sh +++ b/scripts/server-setup.sh @@ -36,3 +36,5 @@ cargo add diesel --features "diesel/postgres diesel/r2d2 diesel/serde_json diese cargo build echo "--------cargo build finished-------------" +echo "------------mischellaneous---------------" +touch docker-compose/localstack/export_cyphers.sh diff --git a/src/db/utils.rs b/src/db/utils.rs index f0dc13a64..74815a05f 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -4,6 +4,8 @@ use diesel::{ PgConnection, }; +use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; + //TODOP separate out AppState from DB pub struct AppState { pub db: Addr, @@ -16,9 +18,23 @@ impl Actor for DbActor { type Context = SyncContext; } -pub fn get_pool(db_url: &str) -> Pool> { +pub async fn get_pool() -> Pool> { + let db_url = get_database_url().await; let manager: ConnectionManager = ConnectionManager::::new(db_url); Pool::builder() .build(manager) .expect("Error building a connection pool") } + +async fn get_database_url() -> String { + let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); + let kms_client = kms::new_client(); + let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); + let db_password = match app_env.as_str() { + "DEV" => String::from("docker"), + _ => kms::decrypt(kms_client, "AWS_REGION").await, + }; + let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); + let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); + format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") +} diff --git a/src/main.rs b/src/main.rs index c327a7fde..aa085fbcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ use api::derived::{ }; use dotenv; -use std::env; use std::io::Result; use actix::SyncArbiter; @@ -29,8 +28,7 @@ use v1::api::*; async fn main() -> Result<()> { dotenv::dotenv().ok(); env_logger::init(); - let db_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set in environment"); - let pool = get_pool(&db_url); + let pool = get_pool().await; let pool_cl = pool.clone(); let db_addr = SyncArbiter::start(5, move || DbActor(pool_cl.clone())); HttpServer::new(move || { diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 67bcf577f..c84e69ade 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -9,22 +9,21 @@ use crate::{ }, }; use actix_web::{ - put, get, + error, get, put, web::{self, Data}, - HttpResponse, Scope, Result, Responder, error + HttpResponse, Responder, Result, Scope, }; use chrono::Utc; use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, - RunQueryDsl, - QueryDsl, ExpressionMethods, QueryResult + ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl, }; use serde_json::json; pub fn endpoints() -> Scope { Scope::new("") - .service(add_contexts_overrides) - .service(get_context) + .service(add_contexts_overrides) + .service(get_context) } #[put("add")] @@ -91,10 +90,7 @@ async fn add_contexts_overrides( } #[get("/{ctx_id}")] -async fn get_context( - path: web::Path, - state: Data -) -> Result { +async fn get_context(path: web::Path, state: Data) -> Result { use crate::v1::db::schema::contexts::dsl::*; let ctx_id = path.into_inner(); @@ -106,20 +102,18 @@ async fn get_context( } }; - let result: QueryResult> = contexts - .filter(id.eq(ctx_id)) - .load(&mut conn); + let result: QueryResult> = contexts.filter(id.eq(ctx_id)).load(&mut conn); let ctx_vec = match result { - Ok(ctx_vec) => ctx_vec, - Err(e) => { + Ok(ctx_vec) => ctx_vec, + Err(e) => { println!("Failed to execute query, error: {e}"); return Err(error::ErrorInternalServerError("")); } }; match ctx_vec.first() { - Some(ctx) => Ok(web::Json(ctx.clone())), - _ => Err(error::ErrorNotFound("")) + Some(ctx) => Ok(web::Json(ctx.clone())), + _ => Err(error::ErrorNotFound("")), } } diff --git a/src/v1/aws/kms.rs b/src/v1/aws/kms.rs new file mode 100644 index 000000000..a3bf848ee --- /dev/null +++ b/src/v1/aws/kms.rs @@ -0,0 +1,48 @@ +use crate::v1::helpers::get_from_env_unsafe; +use bytes::Bytes; +use rusoto_kms::{DecryptRequest, DecryptResponse, Kms, KmsClient}; +use rusoto_signature::region::Region; + +//static REGION: Region = get_from_env_unsafe("AWS_REGION").expect("error: AWS_REGION env not found"); +//static KMS_CLIENT: KmsClient = KmsClient::new(REGION); + +pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { + let cypher: String = get_from_env_unsafe(secret_name) + .expect(format!("{secret_name} not found in env").as_str()); + let req = DecryptRequest { + ciphertext_blob: Bytes::from(cypher), + encryption_algorithm: None, + encryption_context: None, + grant_tokens: None, + //NOTE we use symmetric key encryption therefore key_id is optional + key_id: None, + }; + let decrypt_resp = Kms::decrypt(&client, req).await; + match decrypt_resp { + Ok(DecryptResponse { + plaintext: Some(data), + .. + }) => String::from_utf8(data.to_vec()) + .expect(format!("Could not convert kms val for {secret_name} to utf8").as_str()), + e => panic!("KMS decryption failed for {secret_name} with error {e:?}"), + } +} + +pub fn new_client() -> KmsClient { + //TODO make this an enum and add to appstate + let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); + + let kms_region = match app_env.as_str() { + "DEV" => { + Region::Custom { + name: get_from_env_unsafe("AWS_REGION").unwrap(), + endpoint: "http://localhost:4566".to_owned(), + } + }, + _ => { + get_from_env_unsafe("AWS_REGION").unwrap() + }, + }; + + KmsClient::new(kms_region) +} diff --git a/src/v1/aws/mod.rs b/src/v1/aws/mod.rs new file mode 100644 index 000000000..5550fd59a --- /dev/null +++ b/src/v1/aws/mod.rs @@ -0,0 +1 @@ +pub mod kms; diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 0fbc143a1..94c451f60 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -19,4 +19,7 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!(contexts, overrides,); +diesel::allow_tables_to_appear_in_same_query!( + contexts, + overrides, +); diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs new file mode 100644 index 000000000..f7f9d6b90 --- /dev/null +++ b/src/v1/helpers.rs @@ -0,0 +1,14 @@ +use std::{env::VarError, str::FromStr}; + +//WARN Do NOT use this fxn inside api requests, instead add the required +//env to AppState and get value from there. As this panics, it should +//only be used for envs needed during app start. +pub fn get_from_env_unsafe(name: &str) -> Result +where + F: FromStr, + ::Err: std::fmt::Debug, +{ + std::env::var(name) + .map(|val| val.parse().unwrap()) + .map_err(|e| { println!("{e}"); return e;}) +} diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 037782038..4d9b03d74 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -1,2 +1,4 @@ pub mod api; +pub mod aws; pub mod db; +pub mod helpers; From 91b5cc904abb6a51660890ad7d5c0b9ba28be080 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 8 Jun 2023 14:19:41 +0530 Subject: [PATCH 019/352] build: installing ca-certificates for ssl verification --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2dccde302..36c84cf6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN cargo build --release FROM debian:bullseye RUN apt-get update -RUN apt-get install --yes libpq5 +RUN apt-get install --yes libpq5 ca-certificates COPY --from=builder /app/target/release/context-aware-config /app/context-aware-config WORKDIR /app From 9770384622701373e5d171d782491d232722889f Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 8 Jun 2023 15:27:18 +0530 Subject: [PATCH 020/352] fix: corrected env for DB_PASSWORD and default for AWS_REGION --- src/db/utils.rs | 2 +- src/v1/aws/kms.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/db/utils.rs b/src/db/utils.rs index 74815a05f..273c3125b 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -32,7 +32,7 @@ async fn get_database_url() -> String { let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); let db_password = match app_env.as_str() { "DEV" => String::from("docker"), - _ => kms::decrypt(kms_client, "AWS_REGION").await, + _ => kms::decrypt(kms_client, "DB_PASSWORD").await, }; let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); diff --git a/src/v1/aws/kms.rs b/src/v1/aws/kms.rs index a3bf848ee..a8460133d 100644 --- a/src/v1/aws/kms.rs +++ b/src/v1/aws/kms.rs @@ -35,12 +35,12 @@ pub fn new_client() -> KmsClient { let kms_region = match app_env.as_str() { "DEV" => { Region::Custom { - name: get_from_env_unsafe("AWS_REGION").unwrap(), + name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), endpoint: "http://localhost:4566".to_owned(), } }, _ => { - get_from_env_unsafe("AWS_REGION").unwrap() + get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1) }, }; From dfd45282f5241ada1c810d40f0a4a08b14edb3aa Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 8 Jun 2023 15:59:45 +0530 Subject: [PATCH 021/352] fix: improved log for env not found --- src/v1/aws/kms.rs | 1 + src/v1/helpers.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v1/aws/kms.rs b/src/v1/aws/kms.rs index a8460133d..0ed79542f 100644 --- a/src/v1/aws/kms.rs +++ b/src/v1/aws/kms.rs @@ -9,6 +9,7 @@ use rusoto_signature::region::Region; pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { let cypher: String = get_from_env_unsafe(secret_name) .expect(format!("{secret_name} not found in env").as_str()); + println!("KMS - secret: {secret_name}, cypher: {cypher}"); let req = DecryptRequest { ciphertext_blob: Bytes::from(cypher), encryption_algorithm: None, diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index f7f9d6b90..73c043e32 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -10,5 +10,5 @@ where { std::env::var(name) .map(|val| val.parse().unwrap()) - .map_err(|e| { println!("{e}"); return e;}) + .map_err(|e| { println!("{name} env not found with error: {e}"); return e;}) } From d0ca79dae75790166ec1ea226b24e5f61a35fa6d Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Thu, 8 Jun 2023 16:53:25 +0530 Subject: [PATCH 022/352] ci: Enabling production docker image push. --- Jenkinsfile | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 23fd5b468..62d8ec6ae 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -52,20 +52,19 @@ pipeline { } } - // Disabled for now, as prod setup is not complete. - //stage('Push Image To Production Registry') { - // when { - // expression { SKIP_CI == 'false' } - // branch 'main' - // } - // steps { - // sh '''make ci-push -e \ - // VERSION=${COMMIT_HASH} \ - // REGION=${REGION} \ - // REGISTRY_HOST=${REGISTRY_HOST_PROD} - // ''' - // } - //} + stage('Push Image To Production Registry') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${COMMIT_HASH} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_PROD} + ''' + } + } stage('Create Integ Release Tracker') { when { From fb19e0c992936c166a1a36b0be974664222d1b6b Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 8 Jun 2023 17:00:09 +0530 Subject: [PATCH 023/352] fix: base64 decoding kms cypher --- Cargo.lock | 7 ++++--- Cargo.toml | 1 + src/db/utils.rs | 6 ++---- src/v1/aws/kms.rs | 8 +++----- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 406df37ce..fc5f53cbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64 0.21.0", + "base64 0.21.2", "bitflags 1.3.2", "brotli", "bytes", @@ -328,9 +328,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bitflags" @@ -477,6 +477,7 @@ version = "0.1.0" dependencies = [ "actix", "actix-web", + "base64 0.21.2", "blake3", "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 187b8f861..af9d46980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ rusoto_kms = "0.48.0" rusoto_signature = "0.48.0" bytes = "1.4.0" rusoto_core = "0.48.0" +base64 = "0.21.2" diff --git a/src/db/utils.rs b/src/db/utils.rs index 273c3125b..ecdd70f29 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -30,10 +30,8 @@ async fn get_database_url() -> String { let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); let kms_client = kms::new_client(); let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); - let db_password = match app_env.as_str() { - "DEV" => String::from("docker"), - _ => kms::decrypt(kms_client, "DB_PASSWORD").await, - }; + let db_password = kms::decrypt(kms_client, "DB_PASSWORD").await; + println!("db_password: {}", db_password.len().to_string()); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") diff --git a/src/v1/aws/kms.rs b/src/v1/aws/kms.rs index 0ed79542f..b5edbf443 100644 --- a/src/v1/aws/kms.rs +++ b/src/v1/aws/kms.rs @@ -3,13 +3,11 @@ use bytes::Bytes; use rusoto_kms::{DecryptRequest, DecryptResponse, Kms, KmsClient}; use rusoto_signature::region::Region; -//static REGION: Region = get_from_env_unsafe("AWS_REGION").expect("error: AWS_REGION env not found"); -//static KMS_CLIENT: KmsClient = KmsClient::new(REGION); - +//TODO refactor below code +#[allow(deprecated)] pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { - let cypher: String = get_from_env_unsafe(secret_name) + let cypher = get_from_env_unsafe(secret_name).map(|x: String| base64::decode(x).unwrap()) .expect(format!("{secret_name} not found in env").as_str()); - println!("KMS - secret: {secret_name}, cypher: {cypher}"); let req = DecryptRequest { ciphertext_blob: Bytes::from(cypher), encryption_algorithm: None, From 58c4444f76a68319c86b5560d8726c3c6c519f8d Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 8 Jun 2023 19:02:55 +0530 Subject: [PATCH 024/352] fix: changed host to 0.0.0.0 --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index aa085fbcc..d0c36b03c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ async fn main() -> Result<()> { .service(scope("reduce").service(reduce_contexts_overrides)) .service(scope("promote").service(promote_contexts_overrides)) }) - .bind(("localhost", 8080))? + .bind(("0.0.0.0", 8080))? .workers(5) .run() .await From 8e308ab8c37fb001a0f6b6283ff5b0c547376219 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 12 Jun 2023 12:27:02 +0530 Subject: [PATCH 025/352] ci: moved nixpkgs to nixos-22.11 as the unstable one had broken rustfmt --- flake.lock | 8 ++++---- flake.nix | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 3600e86de..8e18eccab 100644 --- a/flake.lock +++ b/flake.lock @@ -52,16 +52,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1683657389, - "narHash": "sha256-jx91UqqoBneE8QPAKJA29GANrU/Z7ULghoa/JE0+Edw=", + "lastModified": 1686476475, + "narHash": "sha256-W9yUePvCSDghn+YUXewuodyPxt+kJl/a7zdY4Q6r4MU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9524f57dd5b3944c819dd594aed8ed941932ef56", + "rev": "eef86b8a942913a828b9ef13722835f359deef29", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-22.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 65a23d56c..590061cb3 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ inputs = { flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; }; outputs = { self, flake-utils, naersk, nixpkgs }: From a57be1eb4a9f29d1f4d1229aa4f422c3ec85c538 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 29 May 2023 20:30:55 +0530 Subject: [PATCH 026/352] fix: dimension Table scaffolding, PUT<>DELETE apis `/dimension` --- Cargo.lock | 13 ++++ Cargo.toml | 1 + flake.nix | 1 + makefile | 2 +- .../down.sql | 2 + .../up.sql | 9 +++ src/db/utils.rs | 2 - src/main.rs | 13 +++- src/v1/api/dimension/handlers.rs | 76 +++++++++++++++++++ src/v1/api/dimension/mod.rs | 3 + src/v1/api/dimension/types.rs | 10 +++ src/v1/api/mod.rs | 1 + src/v1/aws/kms.rs | 15 ++-- src/v1/db/models.rs | 26 ++++++- src/v1/db/schema.rs | 25 +++++- src/v1/helpers.rs | 5 +- 16 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 migrations/v1/2023-05-29-140304_create_dimensions/down.sql create mode 100644 migrations/v1/2023-05-29-140304_create_dimensions/up.sql create mode 100644 src/v1/api/dimension/handlers.rs create mode 100644 src/v1/api/dimension/mod.rs create mode 100644 src/v1/api/dimension/types.rs diff --git a/Cargo.lock b/Cargo.lock index fc5f53cbe..034dcf7e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,6 +483,7 @@ dependencies = [ "chrono", "derive_more", "diesel", + "diesel-derive-enum", "dotenv", "env_logger", "log", @@ -660,6 +661,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "diesel-derive-enum" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b10c03b954333d05bfd5be1d8a74eae2c9ca77b86e0f1c3a1ea29c49da1d6c2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "diesel_derives" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index af9d46980..adae7f278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ rusoto_signature = "0.48.0" bytes = "1.4.0" rusoto_core = "0.48.0" base64 = "0.21.2" +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } diff --git a/flake.nix b/flake.nix index 590061cb3..e47f4a8b9 100644 --- a/flake.nix +++ b/flake.nix @@ -39,6 +39,7 @@ diesel-cli docker-compose stdenv.cc + # move this to system specific devshell darwin.apple_sdk.frameworks.Security ]; # buildInputs = with pkgs; [ ]; diff --git a/makefile b/makefile index d747c1467..75c962d51 100644 --- a/makefile +++ b/makefile @@ -23,10 +23,10 @@ registry-login: --password-stdin $(REGISTRY_HOST) run: + pkill -f context-aware-config & touch ./docker-compose/localstack/export_cyphers.sh cargo build --color always docker-compose up -d postgres localstack - pkill -f context-aware-config & source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always diff --git a/migrations/v1/2023-05-29-140304_create_dimensions/down.sql b/migrations/v1/2023-05-29-140304_create_dimensions/down.sql new file mode 100644 index 000000000..ecf082d1b --- /dev/null +++ b/migrations/v1/2023-05-29-140304_create_dimensions/down.sql @@ -0,0 +1,2 @@ +DROP TABLE dimensions; +DROP TYPE dimension_type; diff --git a/migrations/v1/2023-05-29-140304_create_dimensions/up.sql b/migrations/v1/2023-05-29-140304_create_dimensions/up.sql new file mode 100644 index 000000000..1f6118e4e --- /dev/null +++ b/migrations/v1/2023-05-29-140304_create_dimensions/up.sql @@ -0,0 +1,9 @@ +CREATE TYPE dimension_type as enum ('NULL', 'BOOL', 'NUMBER', 'STRING', 'ARRAY', 'OBJECT'); + +CREATE TABLE dimensions ( + dimension VARCHAR PRIMARY KEY, + priority integer NOT NULL, + type dimension_type NOT NULL, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR NOT NULL +); diff --git a/src/db/utils.rs b/src/db/utils.rs index ecdd70f29..3a208563f 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -29,9 +29,7 @@ pub async fn get_pool() -> Pool> { async fn get_database_url() -> String { let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); let kms_client = kms::new_client(); - let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); let db_password = kms::decrypt(kms_client, "DB_PASSWORD").await; - println!("db_password: {}", db_password.len().to_string()); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") diff --git a/src/main.rs b/src/main.rs index d0c36b03c..d27b95d28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,9 @@ use dotenv; use std::io::Result; use actix::SyncArbiter; -use actix_web::{middleware::Logger, web::scope, web::Data, App, HttpServer, web::get, HttpResponse}; +use actix_web::{ + middleware::Logger, web::get, web::scope, web::Data, App, HttpResponse, HttpServer, +}; use db::utils::{get_pool, AppState, DbActor}; use v1::api::*; @@ -39,7 +41,10 @@ async fn main() -> Result<()> { db_pool: pool.clone(), })) .wrap(logger) - .route("/health", get().to(|| async { HttpResponse::Ok().body("Health is good :D")})) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) /***************************** Primary api routes *****************************/ .service( scope("/global_config") @@ -65,12 +70,14 @@ async fn main() -> Result<()> { .service(delete_override) .service(get_override), ) - .service(scope("/context").service(context::endpoints())) /***************************** Derived api routes *****************************/ .service(scope("/config").service(get_config)) .service(scope("add_context_overrides").service(add_new_context_override)) .service(scope("reduce").service(reduce_contexts_overrides)) .service(scope("promote").service(promote_contexts_overrides)) + /***************************** V1 Routes *****************************/ + .service(scope("/context").service(context::endpoints())) + .service(scope("/dimension").service(dimension::endpoints())) }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs new file mode 100644 index 000000000..da6645289 --- /dev/null +++ b/src/v1/api/dimension/handlers.rs @@ -0,0 +1,76 @@ +use crate::{ + db::utils::AppState, + v1::{ + api::dimension::types::CreateReq, + db::{models::Dimension, schema::dimensions::dsl::*}, + }, +}; +use actix_web::{ + delete, put, + web::{self, Data}, + HttpResponse, Scope, +}; +use chrono::Utc; +use diesel::{prelude::*, QueryDsl, RunQueryDsl}; + +pub fn endpoints() -> Scope { + Scope::new("").service(create).service(delete) +} + +#[put("")] +async fn create(state: Data, req: web::Json) -> HttpResponse { + let new_dimension = Dimension { + dimension: req.dimension.clone(), + priority: i32::from(req.priority), + type_: req.r#type, + created_by: String::from("some_user"), //TODO update after authentication is added + created_at: Utc::now(), + }; + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + let upsert = diesel::insert_into(dimensions) + .values(&new_dimension) + .on_conflict(dimension) + .do_update() + .set(&new_dimension) + .execute(&mut conn); + + match upsert { + Ok(_) => return HttpResponse::Created().body("Dimension created/updated successfully."), + Err(e) => { + println!("Dimension upsert failed with error: {e}"); + return HttpResponse::InternalServerError().body("Failed to create/udpate dimension\n"); + } + } +} + +#[delete("/{dimension_name}")] +async fn delete(state: Data, path: web::Path) -> HttpResponse { + let dimension_name = path.into_inner(); + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + let delete = diesel::delete(QueryDsl::filter(dimensions, dimension.eq(&dimension_name))) + .execute(&mut conn); + + match delete { + Ok(_) => HttpResponse::Ok().body("Dimension: {dimension_name} deleted successfully"), + Err(e) => { + println!("unable to delete dimension {dimension_name}, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + } +} diff --git a/src/v1/api/dimension/mod.rs b/src/v1/api/dimension/mod.rs new file mode 100644 index 000000000..ebe17b924 --- /dev/null +++ b/src/v1/api/dimension/mod.rs @@ -0,0 +1,3 @@ +mod handlers; +mod types; +pub use handlers::endpoints; diff --git a/src/v1/api/dimension/types.rs b/src/v1/api/dimension/types.rs new file mode 100644 index 000000000..494df71eb --- /dev/null +++ b/src/v1/api/dimension/types.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +use crate::v1::db::models::DimensionType; + +#[derive(Deserialize)] +pub struct CreateReq { + pub dimension: String, + pub priority: u16, + pub r#type: DimensionType, +} diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs index 9efb2ab19..18643155f 100644 --- a/src/v1/api/mod.rs +++ b/src/v1/api/mod.rs @@ -1 +1,2 @@ pub mod context; +pub mod dimension; diff --git a/src/v1/aws/kms.rs b/src/v1/aws/kms.rs index b5edbf443..8f09ee22e 100644 --- a/src/v1/aws/kms.rs +++ b/src/v1/aws/kms.rs @@ -6,7 +6,8 @@ use rusoto_signature::region::Region; //TODO refactor below code #[allow(deprecated)] pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { - let cypher = get_from_env_unsafe(secret_name).map(|x: String| base64::decode(x).unwrap()) + let cypher = get_from_env_unsafe(secret_name) + .map(|x: String| base64::decode(x).unwrap()) .expect(format!("{secret_name} not found in env").as_str()); let req = DecryptRequest { ciphertext_blob: Bytes::from(cypher), @@ -32,15 +33,11 @@ pub fn new_client() -> KmsClient { let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); let kms_region = match app_env.as_str() { - "DEV" => { - Region::Custom { - name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), - endpoint: "http://localhost:4566".to_owned(), - } - }, - _ => { - get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1) + "DEV" => Region::Custom { + name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), + endpoint: "http://localhost:4566".to_owned(), }, + _ => get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1), }; KmsClient::new(kms_region) diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 6c8555a9c..e42921bb0 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -1,8 +1,8 @@ use crate::v1::db::schema::*; use chrono::offset::Utc; use chrono::DateTime; -use diesel::{Insertable, Queryable, Selectable}; -use serde::Serialize; +use diesel::{AsChangeset, Insertable, Queryable, Selectable}; +use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Queryable, Selectable, Insertable, Clone, Serialize)] @@ -25,3 +25,25 @@ pub struct Override { pub created_at: DateTime, pub created_by: String, } + +#[derive(Debug, Clone, Copy, diesel_derive_enum::DbEnum, Deserialize, Serialize)] +#[DbValueStyle = "UPPERCASE"] +#[ExistingTypePath = "crate::v1::db::schema::sql_types::DimensionType"] +pub enum DimensionType { + BOOL, + NUMBER, + STRING, + ARRAY, + OBJECT, +} + +#[derive(Queryable, Selectable, Insertable, AsChangeset)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(dimension))] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub type_: DimensionType, + pub created_at: DateTime, + pub created_by: String, +} diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 94c451f60..4ed108505 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -1,5 +1,11 @@ // @generated automatically by Diesel CLI. +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "dimension_type"))] + pub struct DimensionType; +} + diesel::table! { contexts (id) { id -> Varchar, @@ -10,6 +16,20 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::DimensionType; + + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + #[sql_name = "type"] + type_ -> DimensionType, + created_at -> Timestamptz, + created_by -> Varchar, + } +} + diesel::table! { overrides (id) { id -> Varchar, @@ -19,7 +39,4 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!( - contexts, - overrides, -); +diesel::allow_tables_to_appear_in_same_query!(contexts, dimensions, overrides,); diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index 73c043e32..b0f18c2c1 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -10,5 +10,8 @@ where { std::env::var(name) .map(|val| val.parse().unwrap()) - .map_err(|e| { println!("{name} env not found with error: {e}"); return e;}) + .map_err(|e| { + println!("{name} env not found with error: {e}"); + return e; + }) } From b99f5d6ba9d441b3670a01b5ab59c9734f91d8db Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Mon, 12 Jun 2023 20:19:52 +0530 Subject: [PATCH 027/352] ci: Upgraded nixos to 23.05. --- flake.lock | 8 ++++---- flake.nix | 46 +++++++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/flake.lock b/flake.lock index 8e18eccab..26c06dd40 100644 --- a/flake.lock +++ b/flake.lock @@ -52,16 +52,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1686476475, - "narHash": "sha256-W9yUePvCSDghn+YUXewuodyPxt+kJl/a7zdY4Q6r4MU=", + "lastModified": 1686431482, + "narHash": "sha256-oPVQ/0YP7yC2ztNsxvWLrV+f0NQ2QAwxbrZ+bgGydEM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "eef86b8a942913a828b9ef13722835f359deef29", + "rev": "d3bb401dcfc5a46ce51cdfb5762e70cc75d082d2", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-22.11", + "ref": "nixos-23.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index e47f4a8b9..96db897fb 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ inputs = { flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; - nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05"; }; outputs = { self, flake-utils, naersk, nixpkgs }: @@ -23,26 +23,30 @@ # For `nix develop`: devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; - [ - # Build requirements - rustc - cargo - libiconv - openssl - postgresql_12 - # Extras - rust-analyzer - rustfmt - bacon - cargo-watch - diesel-cli - docker-compose - stdenv.cc - # move this to system specific devshell - darwin.apple_sdk.frameworks.Security - ]; - # buildInputs = with pkgs; [ ]; + nativeBuildInputs = + let + univPkgs = with pkgs; [ + # Build requirements + rustc + cargo + libiconv + openssl + postgresql_12 + # Extras + rust-analyzer + rustfmt + bacon + cargo-watch + diesel-cli + docker-compose + stdenv.cc + pkgconfig + ]; + darwinPkgs = with pkgs; [ + darwin.apple_sdk.frameworks.Security + ]; + in + univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else []); }; } ); From d71107eb9aef62ea4fdfc0f86ffe176d858009b8 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 12 Jun 2023 19:57:32 +0530 Subject: [PATCH 028/352] fix: storing pre calculated priority in contexts table - added restriction for dimension priority to always be greater than 0 --- .../down.sql | 1 + .../up.sql | 1 + src/v1/api/context/handlers.rs | 66 +++++++++++++++---- src/v1/api/context/types.rs | 5 +- src/v1/api/dimension/handlers.rs | 4 ++ src/v1/db/models.rs | 1 + src/v1/db/schema.rs | 7 +- 7 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql create mode 100644 migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql diff --git a/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql b/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql new file mode 100644 index 000000000..7202c6857 --- /dev/null +++ b/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql @@ -0,0 +1 @@ +ALTER TABLE contexts DROP COLUMN priority; diff --git a/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql b/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql new file mode 100644 index 000000000..eeef787e6 --- /dev/null +++ b/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql @@ -0,0 +1 @@ +ALTER TABLE contexts ADD priority integer NOT NULL DEFAULT 1; diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index c84e69ade..6504a4f5c 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -3,8 +3,8 @@ use crate::{ v1::{ api::context::types::{AddContextReq, AddContextResp}, db::{ - models::{Context, Override}, - schema::{contexts::dsl::contexts, overrides::dsl::overrides}, + models::{Context, Override, Dimension}, + schema::{contexts::dsl::contexts, overrides::dsl::overrides, dimensions::dsl::dimensions}, }, }, }; @@ -15,10 +15,11 @@ use actix_web::{ }; use chrono::Utc; use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, result::{DatabaseErrorKind::*, Error::DatabaseError}, - ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl, + ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::json; +use serde_json::{json, Value}; pub fn endpoints() -> Scope { Scope::new("") @@ -26,6 +27,39 @@ pub fn endpoints() -> Scope { .service(get_context) } +type DBConnection = PooledConnection>; + +fn val_dimensions_cal_priority( + conn: &mut DBConnection, + cond: &Value, +) -> Result { + let mut get_priority = |key: &String, val: &Value| -> Result { + if key == "var" { + let dimension_name = val + .as_str() + .ok_or_else(|| "Dimension name should be of String type")?; + dimensions.find(dimension_name) + .first(conn) + .map(|d: Dimension| d.priority) + .map_err(|e| format!("{dimension_name}: {}",e.to_string())) + } else { + val_dimensions_cal_priority(conn, val) + } + }; + + match cond { + Value::Object(x) => + x.iter().try_fold(0, |acc, (key, val)| + get_priority(key, val).map(|res| res + acc) + ), + Value::Array(x) => + x.iter().try_fold(0, |acc, item| + val_dimensions_cal_priority(conn, item).map(|res| res + acc) + ), + _ => Ok(0), + } +} + #[put("add")] async fn add_contexts_overrides( req: web::Json, @@ -34,6 +68,20 @@ async fn add_contexts_overrides( let ctxt_cond = json!({ "and": req.context }); + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + let priority = match val_dimensions_cal_priority(&mut conn, &ctxt_cond) { + Ok(0) => { return HttpResponse::BadRequest().body("No dimension found in contexts"); }, + Err(e) => { return HttpResponse::BadRequest().body(e); } + Ok(p) => p, + }; let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); @@ -47,19 +95,12 @@ async fn add_contexts_overrides( let new_ctxt = Context { id: context_id.clone(), value: ctxt_cond, + priority, override_id: override_id.clone(), created_at: Utc::now(), created_by: "some_user".to_string(), }; - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - println!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - }; - let txn = conn.build_transaction().run(|conn| { diesel::insert_into(overrides) .values(&new_override) @@ -73,6 +114,7 @@ async fn add_contexts_overrides( let resp = AddContextResp { context_id, override_id, + priority, }; match txn { diff --git a/src/v1/api/context/types.rs b/src/v1/api/context/types.rs index c695e0a1f..1c16bc7c8 100644 --- a/src/v1/api/context/types.rs +++ b/src/v1/api/context/types.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{ Value, Map}; #[derive(Deserialize)] pub struct AddContextReq { - pub context: Vec, + pub context: Vec>, pub r#override: Value, } @@ -11,4 +11,5 @@ pub struct AddContextReq { pub struct AddContextResp { pub context_id: String, pub override_id: String, + pub priority: i32, } diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs index da6645289..6b8a00168 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/src/v1/api/dimension/handlers.rs @@ -19,6 +19,10 @@ pub fn endpoints() -> Scope { #[put("")] async fn create(state: Data, req: web::Json) -> HttpResponse { + //TODO move this to the type itself rather than special if check + if req.priority <= 0 { + return HttpResponse::BadRequest().body("Priority should be greater than 0"); + } let new_dimension = Dimension { dimension: req.dimension.clone(), priority: i32::from(req.priority), diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index e42921bb0..dba1aab8a 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -14,6 +14,7 @@ pub struct Context { pub override_id: String, pub created_at: DateTime, pub created_by: String, + pub priority: i32, } #[derive(Queryable, Selectable, Insertable)] diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 4ed108505..557b48932 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -13,6 +13,7 @@ diesel::table! { override_id -> Varchar, created_at -> Timestamptz, created_by -> Varchar, + priority -> Int4, } } @@ -39,4 +40,8 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!(contexts, dimensions, overrides,); +diesel::allow_tables_to_appear_in_same_query!( + contexts, + dimensions, + overrides, +); From 9f3a771aee63999749d7974d2a587a705f8f1c4d Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 13 Jun 2023 16:47:55 +0530 Subject: [PATCH 029/352] fix: removed delete dimension api --- src/v1/api/dimension/handlers.rs | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs index 6b8a00168..9502d929a 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/src/v1/api/dimension/handlers.rs @@ -6,15 +6,15 @@ use crate::{ }, }; use actix_web::{ - delete, put, + put, web::{self, Data}, HttpResponse, Scope, }; use chrono::Utc; -use diesel::{prelude::*, QueryDsl, RunQueryDsl}; +use diesel::{RunQueryDsl}; pub fn endpoints() -> Scope { - Scope::new("").service(create).service(delete) + Scope::new("").service(create) } #[put("")] @@ -54,27 +54,3 @@ async fn create(state: Data, req: web::Json) -> HttpRespons } } } - -#[delete("/{dimension_name}")] -async fn delete(state: Data, path: web::Path) -> HttpResponse { - let dimension_name = path.into_inner(); - - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - println!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - }; - - let delete = diesel::delete(QueryDsl::filter(dimensions, dimension.eq(&dimension_name))) - .execute(&mut conn); - - match delete { - Ok(_) => HttpResponse::Ok().body("Dimension: {dimension_name} deleted successfully"), - Err(e) => { - println!("unable to delete dimension {dimension_name}, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - } -} From acb9df38e018739a8753ea3ed3c7fcb227a820cd Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 14 Jun 2023 13:22:01 +0530 Subject: [PATCH 030/352] feat: /default-config/ PUT api --- .../down.sql | 1 + .../up.sql | 6 +++ src/main.rs | 2 + src/v1/api/default_config/handlers.rs | 40 +++++++++++++++++++ src/v1/api/default_config/mod.rs | 3 ++ src/v1/api/default_config/types.rs | 7 ++++ src/v1/api/mod.rs | 1 + src/v1/db/models.rs | 10 +++++ src/v1/db/schema.rs | 10 +++++ 9 files changed, 80 insertions(+) create mode 100644 migrations/v1/2023-06-14-054049_create-default_configs/down.sql create mode 100644 migrations/v1/2023-06-14-054049_create-default_configs/up.sql create mode 100644 src/v1/api/default_config/handlers.rs create mode 100644 src/v1/api/default_config/mod.rs create mode 100644 src/v1/api/default_config/types.rs diff --git a/migrations/v1/2023-06-14-054049_create-default_configs/down.sql b/migrations/v1/2023-06-14-054049_create-default_configs/down.sql new file mode 100644 index 000000000..edc7b697f --- /dev/null +++ b/migrations/v1/2023-06-14-054049_create-default_configs/down.sql @@ -0,0 +1 @@ +DROP TABLE default_configs; diff --git a/migrations/v1/2023-06-14-054049_create-default_configs/up.sql b/migrations/v1/2023-06-14-054049_create-default_configs/up.sql new file mode 100644 index 000000000..128534967 --- /dev/null +++ b/migrations/v1/2023-06-14-054049_create-default_configs/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS default_configs ( + key varchar PRIMARY KEY, + value JSON NOT NULL, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by varchar NOT NULL +); diff --git a/src/main.rs b/src/main.rs index d27b95d28..84c889e16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,6 +78,8 @@ async fn main() -> Result<()> { /***************************** V1 Routes *****************************/ .service(scope("/context").service(context::endpoints())) .service(scope("/dimension").service(dimension::endpoints())) + .service(scope("/default-config").service(default_config::endpoints())) + }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/src/v1/api/default_config/handlers.rs b/src/v1/api/default_config/handlers.rs new file mode 100644 index 000000000..bef40847b --- /dev/null +++ b/src/v1/api/default_config/handlers.rs @@ -0,0 +1,40 @@ +use actix_web::{Scope, web::{self, Data}, HttpResponse, put}; +use chrono::Utc; +use crate::{db::utils::AppState, v1::db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}}; +use diesel::{RunQueryDsl}; +use super::types::CreateReq; + +pub fn endpoints() -> Scope { + Scope::new("").service(create) +} + +#[put("/{key}")] +async fn create(state: Data, key: web::Path, request: web::Json ) -> HttpResponse { + let req = request.into_inner(); + let new_default_config = DefaultConfig { + key: key.into_inner(), + value: req.value, + created_by: String::from("some_user"), //TODO update after authentication is added + created_at: Utc::now(), + }; + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + let upsert = diesel::insert_into(default_configs) + .values(&new_default_config) + .execute(&mut conn); + + match upsert { + Ok(_) => return HttpResponse::Created().body("DefaultConfig created successfully."), + Err(e) => { + println!("DefaultConfig creation failed with error: {e}"); + return HttpResponse::InternalServerError().body("Failed to create DefaultConfig"); + } + } +} diff --git a/src/v1/api/default_config/mod.rs b/src/v1/api/default_config/mod.rs new file mode 100644 index 000000000..ebe17b924 --- /dev/null +++ b/src/v1/api/default_config/mod.rs @@ -0,0 +1,3 @@ +mod handlers; +mod types; +pub use handlers::endpoints; diff --git a/src/v1/api/default_config/types.rs b/src/v1/api/default_config/types.rs new file mode 100644 index 000000000..8495fe50a --- /dev/null +++ b/src/v1/api/default_config/types.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; +use serde_json::Value; + +#[derive(Deserialize)] +pub struct CreateReq { + pub value: Value, +} diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs index 18643155f..75a293120 100644 --- a/src/v1/api/mod.rs +++ b/src/v1/api/mod.rs @@ -1,2 +1,3 @@ pub mod context; pub mod dimension; +pub mod default_config; diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index dba1aab8a..0970410a4 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -48,3 +48,13 @@ pub struct Dimension { pub created_at: DateTime, pub created_by: String, } + +#[derive(Queryable, Selectable, Insertable, AsChangeset)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(key))] +pub struct DefaultConfig { + pub key: String, + pub value: Value, + pub created_at: DateTime, + pub created_by: String, +} diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 557b48932..2352b6cfd 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -17,6 +17,15 @@ diesel::table! { } } +diesel::table! { + default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + } +} + diesel::table! { use diesel::sql_types::*; use super::sql_types::DimensionType; @@ -42,6 +51,7 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( contexts, + default_configs, dimensions, overrides, ); From 0cdbc3c7ae519e69ca43dfaa23fbb5b9ee861c52 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 14 Jun 2023 18:38:42 +0530 Subject: [PATCH 031/352] feat: GET /config api - response will have overrides, contexts, global_configs --- src/main.rs | 4 +-- src/v1/api/config/handlers.rs | 68 +++++++++++++++++++++++++++++++++++ src/v1/api/config/mod.rs | 3 ++ src/v1/api/config/types.rs | 16 +++++++++ src/v1/api/mod.rs | 1 + src/v1/helpers.rs | 26 +++++++++++++- 6 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/v1/api/config/handlers.rs create mode 100644 src/v1/api/config/mod.rs create mode 100644 src/v1/api/config/types.rs diff --git a/src/main.rs b/src/main.rs index 84c889e16..6cb80a366 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,7 @@ async fn main() -> Result<()> { .service(get_override), ) /***************************** Derived api routes *****************************/ - .service(scope("/config").service(get_config)) + .service(scope("/config-old").service(get_config)) .service(scope("add_context_overrides").service(add_new_context_override)) .service(scope("reduce").service(reduce_contexts_overrides)) .service(scope("promote").service(promote_contexts_overrides)) @@ -79,7 +79,7 @@ async fn main() -> Result<()> { .service(scope("/context").service(context::endpoints())) .service(scope("/dimension").service(dimension::endpoints())) .service(scope("/default-config").service(default_config::endpoints())) - + .service(scope("/config").service(config::endpoints())) }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs new file mode 100644 index 000000000..da8582d0f --- /dev/null +++ b/src/v1/api/config/handlers.rs @@ -0,0 +1,68 @@ +use crate::{ + db::utils::AppState, + v1::{db::{ + schema::{default_configs::dsl as def_conf, contexts::dsl as ctxt, overrides::dsl as over_dsl},//TODO rename over_dsl + }, helpers::ToActixErr} +}; +use super::types::Config; +use actix_web::{ + get, + web::{Data, Json}, + Scope +}; +use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; +use serde_json::{Map, Value, Value::Null}; + +pub fn endpoints() -> Scope { + Scope::new("").service(get) +} + +#[get("")] +async fn get(state: Data) -> actix_web::Result> { + let mut conn = state + .db_pool.get() + .map_err_to_internal_server("error getting a connection from db pool", Null)?; + + let overrides_vec = over_dsl::overrides + .select((over_dsl::id, over_dsl::value)) + .load::<(String, Value)>(&mut conn) + .map_err_to_internal_server("error getting overrides", Null)?; + + let overrides = overrides_vec.into_iter().fold(Map::new(), |mut acc, item| { + acc.insert(item.0, item.1); + acc + }); + + let contexts_vec = ctxt::contexts + .select((ctxt::value, ctxt::override_id)) + .order_by(ctxt::priority.asc()) + .load::<(Value, String)>(&mut conn) + .map_err_to_internal_server("error getting contexts", Null)?; + + let contexts = contexts_vec.into_iter().fold(Vec::new(), |mut acc, item| { + let ctxt = super::types::Context { + condition: item.0, + override_with_keys: [item.1] + }; + acc.push(ctxt); + acc + }); + + let default_config_vec = def_conf::default_configs + .select((def_conf::key, def_conf::value)) + .load::<(String, Value)>(&mut conn) + .map_err_to_internal_server("error getting default configs", Null)?; + + let default_configs = default_config_vec.into_iter().fold(Map::new(), |mut acc, item| { + acc.insert(item.0, item.1); + acc + }); + + let config = Config { + contexts, + overrides, + default_configs + }; + + Ok(Json(config)) +} diff --git a/src/v1/api/config/mod.rs b/src/v1/api/config/mod.rs new file mode 100644 index 000000000..ebe17b924 --- /dev/null +++ b/src/v1/api/config/mod.rs @@ -0,0 +1,3 @@ +mod handlers; +mod types; +pub use handlers::endpoints; diff --git a/src/v1/api/config/types.rs b/src/v1/api/config/types.rs new file mode 100644 index 000000000..f27d4b073 --- /dev/null +++ b/src/v1/api/config/types.rs @@ -0,0 +1,16 @@ +use serde::Serialize; +use serde_json::{ Map, Value}; + +#[derive(Serialize)] +pub struct Config { + pub contexts: Vec, + pub overrides: Map, + pub default_configs: Map +} + +#[derive(Serialize)] +pub struct Context { + pub condition: Value, + pub override_with_keys: [String; 1], +} + diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs index 75a293120..654a075a0 100644 --- a/src/v1/api/mod.rs +++ b/src/v1/api/mod.rs @@ -1,3 +1,4 @@ pub mod context; pub mod dimension; pub mod default_config; +pub mod config; diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index b0f18c2c1..6c2eb1439 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -1,4 +1,6 @@ -use std::{env::VarError, str::FromStr}; +use std::{env::VarError, str::FromStr, fmt}; + +use actix_web::{Error, error::ErrorInternalServerError}; //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should @@ -15,3 +17,25 @@ where return e; }) } + +pub trait ToActixErr { + fn map_err_to_internal_server(self, log_prefix: &str, err_body: B) -> Result + where + B: fmt::Debug + fmt::Display + 'static, + ; +} + +impl ToActixErr for Result +where + E: fmt::Debug, +{ + fn map_err_to_internal_server(self, log_prefix: &str, err_body: B) -> Result + where + B: fmt::Debug + fmt::Display + 'static, + { + self.map_err(|e| { + println!("{log_prefix}, err: {e:?}"); + ErrorInternalServerError(err_body) + }) + } +} From b52a121f244b301a2067e32201bee4fccf653424 Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Thu, 15 Jun 2023 21:01:52 +0530 Subject: [PATCH 032/352] fix: DB password URI encoded. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/db/utils.rs | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 034dcf7e7..320a85052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,6 +494,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "urlencoding", "uuid", ] @@ -2127,6 +2128,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "uuid" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index adae7f278..f34c70ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,4 @@ bytes = "1.4.0" rusoto_core = "0.48.0" base64 = "0.21.2" diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = "~2.1.2" \ No newline at end of file diff --git a/src/db/utils.rs b/src/db/utils.rs index 3a208563f..baa5df346 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -3,6 +3,7 @@ use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; +use urlencoding::encode; use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; @@ -29,7 +30,8 @@ pub async fn get_pool() -> Pool> { async fn get_database_url() -> String { let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); let kms_client = kms::new_client(); - let db_password = kms::decrypt(kms_client, "DB_PASSWORD").await; + let db_password_raw = kms::decrypt(kms_client, "DB_PASSWORD").await; + let db_password = encode(db_password_raw.as_str()).to_string(); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") From a89b48599d0eb63f9c89c664987d96f77feabfae Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Tue, 30 May 2023 15:44:58 +0530 Subject: [PATCH 033/352] feat: Added context list API - Exposed GET@/context/list?page=&size= --- src/v1/api/context/handlers.rs | 80 +++++++++++++++++++++++++--------- src/v1/api/context/types.rs | 6 +++ 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 6504a4f5c..df60c940e 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -1,11 +1,14 @@ use crate::{ db::utils::AppState, v1::{ - api::context::types::{AddContextReq, AddContextResp}, + api::context::types::{AddContextReq, AddContextResp, PaginationParams}, db::{ - models::{Context, Override, Dimension}, - schema::{contexts::dsl::contexts, overrides::dsl::overrides, dimensions::dsl::dimensions}, + models::{Context, Dimension, Override}, + schema::{ + contexts::dsl::contexts, dimensions::dsl::dimensions, overrides::dsl::overrides, + }, }, + helpers::ToActixErr, }, }; use actix_web::{ @@ -19,43 +22,40 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::{json, Value}; +use serde_json::{json, Value, Value::Null}; pub fn endpoints() -> Scope { Scope::new("") .service(add_contexts_overrides) .service(get_context) + .service(list_contexts) } type DBConnection = PooledConnection>; -fn val_dimensions_cal_priority( - conn: &mut DBConnection, - cond: &Value, -) -> Result { +fn val_dimensions_cal_priority(conn: &mut DBConnection, cond: &Value) -> Result { let mut get_priority = |key: &String, val: &Value| -> Result { if key == "var" { let dimension_name = val .as_str() .ok_or_else(|| "Dimension name should be of String type")?; - dimensions.find(dimension_name) + dimensions + .find(dimension_name) .first(conn) .map(|d: Dimension| d.priority) - .map_err(|e| format!("{dimension_name}: {}",e.to_string())) + .map_err(|e| format!("{dimension_name}: {}", e.to_string())) } else { val_dimensions_cal_priority(conn, val) } }; match cond { - Value::Object(x) => - x.iter().try_fold(0, |acc, (key, val)| - get_priority(key, val).map(|res| res + acc) - ), - Value::Array(x) => - x.iter().try_fold(0, |acc, item| - val_dimensions_cal_priority(conn, item).map(|res| res + acc) - ), + Value::Object(x) => x.iter().try_fold(0, |acc, (key, val)| { + get_priority(key, val).map(|res| res + acc) + }), + Value::Array(x) => x.iter().try_fold(0, |acc, item| { + val_dimensions_cal_priority(conn, item).map(|res| res + acc) + }), _ => Ok(0), } } @@ -78,8 +78,12 @@ async fn add_contexts_overrides( }; let priority = match val_dimensions_cal_priority(&mut conn, &ctxt_cond) { - Ok(0) => { return HttpResponse::BadRequest().body("No dimension found in contexts"); }, - Err(e) => { return HttpResponse::BadRequest().body(e); } + Ok(0) => { + return HttpResponse::BadRequest().body("No dimension found in contexts"); + } + Err(e) => { + return HttpResponse::BadRequest().body(e); + } Ok(p) => p, }; let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); @@ -159,3 +163,39 @@ async fn get_context(path: web::Path, state: Data) -> Result Err(error::ErrorNotFound("")), } } + +#[get("/list")] +async fn list_contexts( + qparams: web::Query, + state: Data, +) -> Result { + use crate::v1::db::schema::contexts::dsl::*; + + let mut conn = state + .db_pool + .get() + .map_err_to_internal_server("Unable to get db connection from pool", Null)?; + + let PaginationParams { + page: opt_page, + size: opt_size, + } = qparams.into_inner(); + let default_page = 1; + let page = opt_page.unwrap_or(default_page); + let default_size = 20; + let size = opt_size.unwrap_or(default_size); + + if page < 1 { + return Err(error::ErrorBadRequest("Param 'page' has to be at least 1.")); + } else if size < 1 { + return Err(error::ErrorBadRequest("Param 'size' has to be at least 1.")); + } + + let result: Vec = contexts + .order(created_at) + .limit(i64::from(size)) + .offset(i64::from(size * (page - 1))) + .load(&mut conn) + .map_err_to_internal_server("Failed to execute query, error", Null)?; + Ok(web::Json(result)) +} diff --git a/src/v1/api/context/types.rs b/src/v1/api/context/types.rs index 1c16bc7c8..7c10e8f0a 100644 --- a/src/v1/api/context/types.rs +++ b/src/v1/api/context/types.rs @@ -13,3 +13,9 @@ pub struct AddContextResp { pub override_id: String, pub priority: i32, } + +#[derive(Deserialize)] +pub struct PaginationParams { + pub page: Option, + pub size: Option +} From c542af8960417abdbe86915a487964a906e6d8ee Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Thu, 15 Jun 2023 15:57:10 +0530 Subject: [PATCH 034/352] feat: Added support for validation via JSON schema. --- Cargo.lock | 490 +++++++++++++++++- Cargo.toml | 4 +- .../down.sql | 2 + .../up.sql | 2 + src/db/utils.rs | 2 + src/main.rs | 3 +- src/v1/api/default_config/handlers.rs | 48 +- src/v1/api/default_config/helpers.rs | 23 + src/v1/api/default_config/mod.rs | 1 + src/v1/api/default_config/types.rs | 3 +- src/v1/db/models.rs | 1 + src/v1/db/schema.rs | 1 + src/v1/helpers.rs | 40 ++ 13 files changed, 610 insertions(+), 10 deletions(-) create mode 100644 migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql create mode 100644 migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql create mode 100644 src/v1/api/default_config/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 320a85052..3566cf1c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "cfg-if", "getrandom", "once_cell", + "serde", "version_check", ] @@ -280,6 +281,61 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "arrayref" version = "0.3.7" @@ -332,6 +388,21 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -403,6 +474,12 @@ version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "byteorder" version = "1.4.3" @@ -455,6 +532,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +dependencies = [ + "anstream", + "anstyle", + "bitflags 1.3.2", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -465,6 +584,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "constant_time_eq" version = "0.2.5" @@ -486,7 +611,9 @@ dependencies = [ "diesel-derive-enum", "dotenv", "env_logger", + "jsonschema", "log", + "reqwest", "rusoto_core", "rusoto_kms", "rusoto_signature", @@ -495,7 +622,7 @@ dependencies = [ "strum", "strum_macros", "urlencoding", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -659,7 +786,7 @@ dependencies = [ "pq-sys", "r2d2", "serde_json", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -785,6 +912,16 @@ dependencies = [ "libc", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -834,6 +971,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures" version = "0.3.28" @@ -940,8 +1087,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1079,6 +1228,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1156,6 +1318,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1180,6 +1369,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48354c4c4f088714424ddf090de1ff84acc82b2f08c192d46d226ae2529a465" +dependencies = [ + "ahash 0.8.3", + "anyhow", + "base64 0.21.2", + "bytecount", + "clap", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time 0.3.21", + "url", + "uuid 1.3.4", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1273,6 +1492,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1312,6 +1537,56 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1322,6 +1597,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1570,6 +1868,63 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rusoto_core" version = "0.48.0" @@ -1676,6 +2031,37 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -1718,6 +2104,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.9.1" @@ -1854,6 +2250,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.24.1" @@ -2037,6 +2445,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -2117,6 +2535,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -2134,6 +2558,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.8.2" @@ -2144,6 +2574,12 @@ dependencies = [ "serde", ] +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2203,6 +2639,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.85" @@ -2232,6 +2680,35 @@ version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +[[package]] +name = "web-sys" +version = "0.3.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2419,6 +2896,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "xml-rs" version = "0.8.14" diff --git a/Cargo.toml b/Cargo.toml index f34c70ee3..e9c8c4ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,6 @@ bytes = "1.4.0" rusoto_core = "0.48.0" base64 = "0.21.2" diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } -urlencoding = "~2.1.2" \ No newline at end of file +urlencoding = "~2.1.2" +jsonschema = "~0.17" +reqwest = { version = "*", features = [ "rustls-tls" ] } \ No newline at end of file diff --git a/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql b/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql new file mode 100644 index 000000000..3c1193ab7 --- /dev/null +++ b/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE default_configs + DROP COLUMN schema; diff --git a/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql b/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql new file mode 100644 index 000000000..c46d86bf1 --- /dev/null +++ b/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE default_configs + ADD COLUMN schema JSON NOT NULL DEFAULT '{}'; diff --git a/src/db/utils.rs b/src/db/utils.rs index baa5df346..735e6f2bd 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -4,6 +4,7 @@ use diesel::{ PgConnection, }; use urlencoding::encode; +use jsonschema::JSONSchema; use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; @@ -11,6 +12,7 @@ use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; pub struct AppState { pub db: Addr, pub db_pool: Pool>, + pub default_config_validation_schema: JSONSchema } pub struct DbActor(pub Pool>); diff --git a/src/main.rs b/src/main.rs index 6cb80a366..96c3d7c9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use actix_web::{ }; use db::utils::{get_pool, AppState, DbActor}; -use v1::api::*; +use v1::{api::*, helpers::get_validation_schema}; #[actix_web::main] async fn main() -> Result<()> { @@ -39,6 +39,7 @@ async fn main() -> Result<()> { .app_data(Data::new(AppState { db: db_addr.clone(), db_pool: pool.clone(), + default_config_validation_schema: get_validation_schema(), })) .wrap(logger) .route( diff --git a/src/v1/api/default_config/handlers.rs b/src/v1/api/default_config/handlers.rs index bef40847b..76693e59d 100644 --- a/src/v1/api/default_config/handlers.rs +++ b/src/v1/api/default_config/handlers.rs @@ -1,19 +1,57 @@ -use actix_web::{Scope, web::{self, Data}, HttpResponse, put}; -use chrono::Utc; -use crate::{db::utils::AppState, v1::db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}}; -use diesel::{RunQueryDsl}; +use super::helpers::validate_schema; use super::types::CreateReq; +use crate::{ + db::utils::AppState, + v1::db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}, +}; +use actix_web::{ + put, + web::{self, Data}, + HttpResponse, Scope, +}; +use chrono::Utc; +use diesel::RunQueryDsl; +use jsonschema::{Draft, JSONSchema}; +use serde_json::Value; pub fn endpoints() -> Scope { Scope::new("").service(create) } #[put("/{key}")] -async fn create(state: Data, key: web::Path, request: web::Json ) -> HttpResponse { +async fn create( + state: Data, + key: web::Path, + request: web::Json, +) -> HttpResponse { let req = request.into_inner(); + let schema = Value::Object(req.schema); + if let Err(e) = validate_schema(&state.default_config_validation_schema, schema.to_owned()) { + return HttpResponse::BadRequest().body(e); + }; + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&schema); + let jschema = match schema_compile_result { + Ok(jschema) => jschema, + Err(e) => { + println!("Failed to compile as a Draft-7 JSON schema: {e}"); + return HttpResponse::BadRequest().body("Bad json schema."); + } + }; + + match jschema.validate(&req.value) { + Ok(_) => (), + Err(_) => { + println!("Validation for value with given JSON schema failed."); + return HttpResponse::BadRequest().body("Validation with given schema failed."); + } + }; + let new_default_config = DefaultConfig { key: key.into_inner(), value: req.value, + schema: schema, created_by: String::from("some_user"), //TODO update after authentication is added created_at: Utc::now(), }; diff --git a/src/v1/api/default_config/helpers.rs b/src/v1/api/default_config/helpers.rs new file mode 100644 index 000000000..b4b1a7212 --- /dev/null +++ b/src/v1/api/default_config/helpers.rs @@ -0,0 +1,23 @@ +use jsonschema::{JSONSchema, ValidationError}; +use serde_json::Value; + +/* + This step is required because an empty object + is also a valid JSON schema. So added required + validations for the input. +*/ +// TODO: Recursive validation. +pub fn validate_schema(validation_schema: &JSONSchema, schema: Value) -> Result<(), String> { + let res = match validation_schema.validate(&schema) { + Ok(_) => Ok(()), + Err(e) => { + //TODO: Try & render as json. + let verrors = e.collect::>(); + Err(String::from(format!( + "Bad schema: {:?}", + verrors.as_slice() + ))) + } + }; + res +} diff --git a/src/v1/api/default_config/mod.rs b/src/v1/api/default_config/mod.rs index ebe17b924..25f79f70b 100644 --- a/src/v1/api/default_config/mod.rs +++ b/src/v1/api/default_config/mod.rs @@ -1,3 +1,4 @@ mod handlers; mod types; +mod helpers; pub use handlers::endpoints; diff --git a/src/v1/api/default_config/types.rs b/src/v1/api/default_config/types.rs index 8495fe50a..2aae54cb5 100644 --- a/src/v1/api/default_config/types.rs +++ b/src/v1/api/default_config/types.rs @@ -1,7 +1,8 @@ use serde::Deserialize; -use serde_json::Value; +use serde_json::{Value, Map}; #[derive(Deserialize)] pub struct CreateReq { pub value: Value, + pub schema: Map } diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 0970410a4..3b61ef0a4 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -55,6 +55,7 @@ pub struct Dimension { pub struct DefaultConfig { pub key: String, pub value: Value, + pub schema: Value, pub created_at: DateTime, pub created_by: String, } diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 2352b6cfd..36391e6d5 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -23,6 +23,7 @@ diesel::table! { value -> Json, created_at -> Timestamptz, created_by -> Varchar, + schema -> Json, } } diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index 6c2eb1439..0769d64f2 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -1,6 +1,8 @@ use std::{env::VarError, str::FromStr, fmt}; use actix_web::{Error, error::ErrorInternalServerError}; +use jsonschema::{JSONSchema, Draft}; +use serde_json::json; //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should @@ -39,3 +41,41 @@ where }) } } + +pub fn get_validation_schema() -> JSONSchema { + let my_schema = json!({ + "type": "object", + "properties": { + "type": { + "enum": ["string", "object", "enum", "number", "boolean", "array"] + } + }, + "required": ["type"], + "allOf": [ + { + "if": { + "properties": { "type": { "const": "object" } } + }, + "then": { + "properties": { "properties": { "type": "object" } }, + "required": ["properties"] + } + }, + { + "if": { + "properties": { "type": { "const": "string" } } + }, + "then": { + "properties": { "pattern": { "type": "string" } }, + "required": ["pattern"] + } + }, + // TODO: Add validations for Array types. + ] + }); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&my_schema) + .expect("THE IMPOSSIBLE HAPPENED, failed to compile the schema for the schema!") +} From 800451004d31c818b48500775877ce63d988c3a0 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 20 Jun 2023 19:35:12 +0530 Subject: [PATCH 035/352] feat: rust library - periodic update of cac configs via polling - evaluation of cac --- flake.nix | 1 + libs/rust/Cargo.lock | 1811 +++++++++++++++++++++++++++++ libs/rust/Cargo.toml | 14 + libs/rust/src/eval.rs | 133 +++ libs/rust/src/lib.rs | 122 ++ libs/rust/src/utils/core.rs | 15 + libs/rust/src/utils/deep_merge.rs | 87 ++ libs/rust/src/utils/mod.rs | 2 + 8 files changed, 2185 insertions(+) create mode 100644 libs/rust/Cargo.lock create mode 100644 libs/rust/Cargo.toml create mode 100644 libs/rust/src/eval.rs create mode 100644 libs/rust/src/lib.rs create mode 100644 libs/rust/src/utils/core.rs create mode 100644 libs/rust/src/utils/deep_merge.rs create mode 100644 libs/rust/src/utils/mod.rs diff --git a/flake.nix b/flake.nix index 96db897fb..6122364ae 100644 --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,7 @@ rustfmt bacon cargo-watch + clippy diesel-cli docker-compose stdenv.cc diff --git a/libs/rust/Cargo.lock b/libs/rust/Cargo.lock new file mode 100644 index 000000000..291126140 --- /dev/null +++ b/libs/rust/Cargo.lock @@ -0,0 +1,1811 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.22", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cac" +version = "0.1.0" +dependencies = [ + "actix-web", + "chrono", + "jsonlogic", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.22", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonlogic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0699c6109316a5add3d42ae21ae954f207739f12b68914a0d447b9aa45e6f" +dependencies = [ + "serde_json", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/libs/rust/Cargo.toml b/libs/rust/Cargo.toml new file mode 100644 index 000000000..7cde04bca --- /dev/null +++ b/libs/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cac" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4.3.1" +chrono = "0.4.26" +jsonlogic = "0.5.1" +reqwest = { version = "0.11.18", features = ["json"]} +serde = { version = "1.0.164", features = ["derive"] } +serde_json = "1.0.97" diff --git a/libs/rust/src/eval.rs b/libs/rust/src/eval.rs new file mode 100644 index 000000000..8ef68b4be --- /dev/null +++ b/libs/rust/src/eval.rs @@ -0,0 +1,133 @@ +//NOTE this code is copied over from sdk-config-server with small changes for compatiblity +//TODO refactor, make eval MJOS agnostic + +use std::collections::HashMap; + +use crate::{utils::deep_merge::{deep_merge, patch_json}, Context}; +use jsonlogic; +use serde::{de, Deserialize, Serialize}; +use serde_json::{from_value, json, to_value, Map, Value}; + +#[derive(Deserialize, Serialize)] +pub struct PathParams { + merchant_id: String, + platform: String, + environment: String, +} + +#[derive(Clone, Debug, Deserialize)] +struct MJOSOverride { + path: Vec, + value: Value, + etag: Option, +} + +fn create_nested_json(path_array: Vec, value: Value) -> serde_json::Result { + path_array.iter().rev().try_fold(value, |acc, key_string| { + to_value(HashMap::::from([( + key_string.to_string(), + acc, + )])) + }) +} + +type EtagMap = HashMap; +pub type Patch = (Value, EtagMap); + +fn form_override_object(input_config: Value) -> serde_json::Result { + let input_map: Map = from_value(input_config)?; + let patch_entries = input_map.get("patch_entries"); + let keys_to_be_overridden: Vec = + patch_entries.map_or(Ok(Vec::new()), |x| from_value(x.to_owned()))?; + let mut etags: EtagMap = HashMap::new(); + let patch_obj = keys_to_be_overridden + .iter() + .try_fold(json!({}), |acc, mapped_key| { + if let Some(MJOSOverride { + path: path_array, + value, + etag: mb_etag, + }) = input_map + .get(mapped_key) + .and_then(|x| -> Option { from_value(x.to_owned()).ok() }) + { + if let (Some(etag), Value::String(url)) = (mb_etag, value.clone()) { + etags.insert(url, etag); + } + let split_path = &path_array + .split_first() + .map(|(first, tail)| (first.as_str(), tail)); + let entries = match split_path { + Some(("live", tail)) => vec![ + path_array.clone(), + [vec!["new".to_string()], tail.to_vec()].concat(), + ], + Some(("new", tail)) => vec![ + path_array.clone(), + [vec!["new".to_string()], tail.to_vec()].concat(), + ], + _ => vec![path_array], + }; + + return entries.iter().try_fold(acc, |accumulator, val| { + deep_merge( + &accumulator, + &create_nested_json(val.to_owned(), json!(value))?, + ) + }); + } + Ok(acc) + }); + patch_obj.map(|x| (x, etags)) +} + +fn choose_merge_strategy( + allow_new_paths: bool, +) -> for<'a, 'b> fn(&'a Value, &'b Value) -> serde_json::Result { + if allow_new_paths { + deep_merge + } else { + patch_json + } +} + +fn get_overrides( + query_data: &Value, + contexts: Vec, + overrides: Map, + allow_new_paths: bool, +) -> serde_json::Result { + let merge_fn = choose_merge_strategy(allow_new_paths); + let mut required_overrides: Value = json!({}); + + for context in contexts.iter() { + // TODO :: Add semantic version comparator in Lib + if let Ok(Value::Bool(true)) = jsonlogic::apply(&context.condition, query_data) { + for override_key in &context.override_with_keys { + if let Some(overriden_value) = overrides.get(override_key) { + required_overrides = merge_fn(&required_overrides, overriden_value)?; + } + } + } + } + + Ok(required_overrides) +} + +pub fn get_mjos_override( + query_data: Value, + contexts: Vec, + overrides: Map, + default_config: Value, +) -> serde_json::Result { + // get_overrides runs json logic and gives patch_entries from overrides.json + let overrides = get_overrides(&query_data, contexts, overrides, true)?; + // patch_entries received from overides after json logic are then merged over default_config + let overrides_config = deep_merge(&default_config, &overrides)?; + + if Some(&json!([])) == overrides_config.get("patch_entries") { + return Err(de::Error::custom("No patches found")); + } + // overriding object is then generated from default_config + form_override_object(overrides_config) +} diff --git a/libs/rust/src/lib.rs b/libs/rust/src/lib.rs new file mode 100644 index 000000000..1e2fd8796 --- /dev/null +++ b/libs/rust/src/lib.rs @@ -0,0 +1,122 @@ +mod utils; +mod eval; + +use actix_web::{ + rt::{ + self, + time::interval, + }, + web::Data, +}; +use eval::{get_mjos_override, Patch}; +use std::{convert::identity, sync::RwLock, time::Duration}; +use utils::core::MapError; +use chrono::{DateTime, Utc}; +use reqwest::{RequestBuilder, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Context { + condition: Value, + override_with_keys: [String; 1], +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + contexts: Vec, + overrides: Map, + default_configs: Map, +} + +#[derive(Clone)] +pub struct Client { + reqw: Data, + polling_interval: Duration, + last_modified: Data>>, + config: Data>, +} + +fn clone_reqw(reqw: &RequestBuilder) -> Result { + reqw.try_clone() + .ok_or_else(|| "Unable to clone reqw".to_string()) +} + +impl Client { + pub async fn new( + update_config_periodically: bool, + polling_interval: Duration, + hostname: String, + ) -> Result { + let reqw_client = reqwest::Client::builder().build().map_err_to_string()?; + let cac_endpoint = format!("{hostname}/config"); + let reqw = reqw_client.get(cac_endpoint); + let reqwc = clone_reqw(&reqw)?; + let resp = reqwc.send().await.map_err_to_string()?; + let config = resp.json::().await.map_err_to_string()?; + let timestamp = Utc::now(); + let client = Client { + reqw: Data::new(reqw), + polling_interval, + last_modified: Data::new(RwLock::new(timestamp)), + config: Data::new(RwLock::new(config)), + }; + if update_config_periodically { + client.clone().start_polling_update().await; + } + Ok(client) + } + + async fn fetch(&self) -> Result { + let last_modified = self.last_modified.read().map_err_to_string()?.to_rfc2822(); + let reqw = clone_reqw(&self.reqw)?.header("If-Modified-Since", last_modified); + let resp = reqw.send().await.map_err_to_string()?; + match resp.status() { + StatusCode::NOT_MODIFIED => { + return Err(String::from("CAC: skipping update, remote not modified")); + } + StatusCode::OK => println!("CAC: new config received, updating"), + x => return Err(format!("CAC: fetch failed, status: {}", x,)), + }; + resp.json::().await.map_err_to_string() + } + + async fn update_cac(&self) -> Result { + let fetched_config = self.fetch().await?; + let mut config = self.config.write().map_err_to_string()?; + let mut last_modified = self.last_modified.write().map_err_to_string()?; + *config = fetched_config; + *last_modified = Utc::now(); + Ok("CAC updated successfully".to_string()) + } + + pub async fn start_polling_update(self) { + rt::spawn(async move { + let mut interval = interval(self.polling_interval); + loop { + interval.tick().await; + let result = self + .update_cac() + .await + .unwrap_or_else(identity); + println!("{result}",); + } + }); + } + + pub fn get_config(&self) -> Result { + self.config.read().map(|c| c.clone()).map_err_to_string() + } + + pub fn get_last_modified(&'static self) -> Result, String> { + self.last_modified + .read() + .map(|t| *t) + .map_err_to_string() + } + + pub fn eval(&self, query_data: Value) -> Result { + let cac = self.config.read().map_err_to_string()?; + get_mjos_override(query_data, cac.contexts.clone(), cac.overrides.clone(), json!(cac.default_configs)).map_err_to_string() + } +} diff --git a/libs/rust/src/utils/core.rs b/libs/rust/src/utils/core.rs new file mode 100644 index 000000000..9437d5d14 --- /dev/null +++ b/libs/rust/src/utils/core.rs @@ -0,0 +1,15 @@ +use std::fmt; + +pub trait MapError { + fn map_err_to_string(self) -> Result; +} + +impl MapError for Result +where + E: fmt::Display, +{ + fn map_err_to_string(self) -> Result { + self.map_err(|e| e.to_string()) + } +} + diff --git a/libs/rust/src/utils/deep_merge.rs b/libs/rust/src/utils/deep_merge.rs new file mode 100644 index 000000000..ec6feaa51 --- /dev/null +++ b/libs/rust/src/utils/deep_merge.rs @@ -0,0 +1,87 @@ +//NOTE this code is copied over from sdk-config-server + +use std::collections::HashMap; + +use serde_json::{from_value, to_value, Error, Value}; + +fn type_of(a: &Value) -> String { + if a.is_boolean() { + return "Boolean".to_string(); + } + + if a.is_number() { + return "Number".to_string(); + } + + if a.is_string() { + return "String".to_string(); + } + + if a.is_array() { + return "Array".to_string(); + } + + if a.is_null() { + return "Null".to_string(); + } + + "Object".to_string() +} + +fn merge_json_helper( + tree1: &Value, + tree2: &Value, + add_new_paths: bool, +) -> Result, Error> { + let mut result_hash_map: HashMap = from_value(tree1.to_owned())?; + + if let Some(subtree) = tree2.as_object() { + for (key, overriden_value) in subtree.iter() { + if let Some(default_value) = tree1.get(key) { + if type_of(default_value) == "Object" { + let merged_sub_tree = + merge_json_helper(default_value, overriden_value, add_new_paths)?; + result_hash_map.insert(key.to_string(), to_value(merged_sub_tree)?); + } else { + result_hash_map.insert(key.to_string(), overriden_value.clone()); + } + } else if add_new_paths { + result_hash_map.insert(key.to_string(), overriden_value.clone()); + } + } + } + + Ok(result_hash_map) +} + +pub fn patch_json(tree1: &Value, tree2: &Value) -> Result { + to_value(merge_json_helper(tree1, tree2, false)?) +} + +pub fn deep_merge(tree1: &Value, tree2: &Value) -> Result { + to_value(merge_json_helper(tree1, tree2, true)?) +} + +// ************ Tests ************* + +#[cfg(test)] +mod tests { + use serde_json::json; + + #[test] + fn patch_json() { + let a = json!({"x":"y"}); + let b = json!({"x":"z", "p":"q"}); + assert_eq!(super::patch_json(&a, &b).unwrap(), json!({"x":"z"})); + } + + #[test] + fn deep_merge() { + let a = json!({"x":"y"}); + let b = json!({"x":"z", "p":"q"}); + assert_eq!( + super::deep_merge(&a, &b).unwrap(), + json!({"x":"z", "p": "q"}) + ); + } +} diff --git a/libs/rust/src/utils/mod.rs b/libs/rust/src/utils/mod.rs new file mode 100644 index 000000000..beafec241 --- /dev/null +++ b/libs/rust/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod deep_merge; +pub mod core; From 83987a02c86eeebbfaa0820d1cf53cd25ce93b74 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 30 Jun 2023 19:01:05 +0530 Subject: [PATCH 036/352] fix: moved override inside contexts table moved overrides inside contexts table and in GET /config endpoint giving them as a separate array of hashed objects itself --- makefile | 4 +- .../down.sql | 7 ++ .../up.sql | 2 + src/v1/api/config/handlers.rs | 66 +++++++++---------- src/v1/api/context/handlers.rs | 30 +++------ src/v1/db/models.rs | 11 +--- src/v1/db/schema.rs | 18 +---- 7 files changed, 55 insertions(+), 83 deletions(-) create mode 100644 migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql create mode 100644 migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql diff --git a/makefile b/makefile index 75c962d51..bf851eb63 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,6 @@ IMAGE_NAME ?= context-aware-config -SHELL := /bin/bash +SHELL := /usr/bin/env bash build: cargo build @@ -23,7 +23,7 @@ registry-login: --password-stdin $(REGISTRY_HOST) run: - pkill -f context-aware-config & + pkill -f target/debug/context-aware-config & touch ./docker-compose/localstack/export_cyphers.sh cargo build --color always docker-compose up -d postgres localstack diff --git a/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql b/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql new file mode 100644 index 000000000..c071e0c3c --- /dev/null +++ b/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE contexts DROP COLUMN override; +CREATE TABLE overrides ( + id VARCHAR PRIMARY KEY, + value JSON NOT NULL, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR NOT NULL +); diff --git a/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql b/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql new file mode 100644 index 000000000..b4a531550 --- /dev/null +++ b/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE contexts ADD override JSON NOT NULL DEFAULT '{}'; +DROP TABLE overrides; diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index da8582d0f..9c3a726fa 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -1,16 +1,17 @@ +use super::types::Config; use crate::{ db::utils::AppState, - v1::{db::{ - schema::{default_configs::dsl as def_conf, contexts::dsl as ctxt, overrides::dsl as over_dsl},//TODO rename over_dsl - }, helpers::ToActixErr} + v1::{ + db::schema::{contexts::dsl as ctxt, default_configs::dsl as def_conf}, + helpers::ToActixErr, + }, }; -use super::types::Config; use actix_web::{ get, web::{Data, Json}, - Scope + Scope, }; -use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value, Value::Null}; pub fn endpoints() -> Scope { @@ -20,48 +21,45 @@ pub fn endpoints() -> Scope { #[get("")] async fn get(state: Data) -> actix_web::Result> { let mut conn = state - .db_pool.get() + .db_pool + .get() .map_err_to_internal_server("error getting a connection from db pool", Null)?; - let overrides_vec = over_dsl::overrides - .select((over_dsl::id, over_dsl::value)) - .load::<(String, Value)>(&mut conn) - .map_err_to_internal_server("error getting overrides", Null)?; - - let overrides = overrides_vec.into_iter().fold(Map::new(), |mut acc, item| { - acc.insert(item.0, item.1); - acc - }); - let contexts_vec = ctxt::contexts - .select((ctxt::value, ctxt::override_id)) + .select((ctxt::value, ctxt::override_id, ctxt::override_)) .order_by(ctxt::priority.asc()) - .load::<(Value, String)>(&mut conn) + .load::<(Value, String, Value)>(&mut conn) .map_err_to_internal_server("error getting contexts", Null)?; - let contexts = contexts_vec.into_iter().fold(Vec::new(), |mut acc, item| { - let ctxt = super::types::Context { - condition: item.0, - override_with_keys: [item.1] - }; - acc.push(ctxt); - acc - }); + let (contexts, overrides) = contexts_vec.into_iter().fold( + (Vec::new(), Map::new()), + |(mut ctxts, mut overrides), (condition, override_id, override_)| { + let ctxt = super::types::Context { + condition, + override_with_keys: [override_id.to_owned()], + }; + ctxts.push(ctxt); + overrides.insert(override_id, override_); + (ctxts, overrides) + }, + ); let default_config_vec = def_conf::default_configs .select((def_conf::key, def_conf::value)) .load::<(String, Value)>(&mut conn) .map_err_to_internal_server("error getting default configs", Null)?; - let default_configs = default_config_vec.into_iter().fold(Map::new(), |mut acc, item| { - acc.insert(item.0, item.1); - acc - }); - - let config = Config { + let default_configs = default_config_vec + .into_iter() + .fold(Map::new(), |mut acc, item| { + acc.insert(item.0, item.1); + acc + }); + + let config = Config { contexts, overrides, - default_configs + default_configs, }; Ok(Json(config)) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index df60c940e..d2c50c5b4 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -3,10 +3,8 @@ use crate::{ v1::{ api::context::types::{AddContextReq, AddContextResp, PaginationParams}, db::{ - models::{Context, Dimension, Override}, - schema::{ - contexts::dsl::contexts, dimensions::dsl::dimensions, overrides::dsl::overrides, - }, + models::{Context, Dimension}, + schema::{contexts::dsl::contexts, dimensions::dsl::dimensions}, }, helpers::ToActixErr, }, @@ -89,31 +87,19 @@ async fn add_contexts_overrides( let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); - let new_override = Override { - id: override_id.clone(), - value: req.r#override.clone(), - created_at: Utc::now(), - created_by: "some_user".to_string(), //TODO update once authentication is added - }; - let new_ctxt = Context { id: context_id.clone(), value: ctxt_cond, priority, - override_id: override_id.clone(), + override_id: override_id.to_owned(), + override_: req.r#override.to_owned(), created_at: Utc::now(), created_by: "some_user".to_string(), }; - let txn = conn.build_transaction().run(|conn| { - diesel::insert_into(overrides) - .values(&new_override) - .on_conflict_do_nothing() - .execute(conn)?; - diesel::insert_into(contexts) - .values(&new_ctxt) - .execute(conn) - }); + let insert = diesel::insert_into(contexts) + .values(&new_ctxt) + .execute(&mut conn); let resp = AddContextResp { context_id, @@ -121,7 +107,7 @@ async fn add_contexts_overrides( priority, }; - match txn { + match insert { Ok(_) => HttpResponse::Created() .insert_header(("x-info", "new context created")) .json(resp), diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 3b61ef0a4..9a11815c2 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -15,16 +15,7 @@ pub struct Context { pub created_at: DateTime, pub created_by: String, pub priority: i32, -} - -#[derive(Queryable, Selectable, Insertable)] -#[diesel(check_for_backend(diesel::pg::Pg))] -#[diesel(primary_key(id))] -pub struct Override { - pub id: String, - pub value: Value, - pub created_at: DateTime, - pub created_by: String, + pub override_: Value, } #[derive(Debug, Clone, Copy, diesel_derive_enum::DbEnum, Deserialize, Serialize)] diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 36391e6d5..c8d205c09 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -14,6 +14,8 @@ diesel::table! { created_at -> Timestamptz, created_by -> Varchar, priority -> Int4, + #[sql_name = "override"] + override_ -> Json, } } @@ -41,18 +43,4 @@ diesel::table! { } } -diesel::table! { - overrides (id) { - id -> Varchar, - value -> Json, - created_at -> Timestamptz, - created_by -> Varchar, - } -} - -diesel::allow_tables_to_appear_in_same_query!( - contexts, - default_configs, - dimensions, - overrides, -); +diesel::allow_tables_to_appear_in_same_query!(contexts, default_configs, dimensions,); From ba3020e0f1a44afda99fd89816ddef7a4b279fd5 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 7 Jul 2023 16:28:16 +0530 Subject: [PATCH 037/352] feat: removed properties constraint on objects in schema because sdk-config-server uses CAC in a way where the flow starts with empty default values --- src/main.rs | 4 +-- src/v1/helpers.rs | 69 +++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/main.rs b/src/main.rs index 96c3d7c9b..3188aec80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use actix_web::{ }; use db::utils::{get_pool, AppState, DbActor}; -use v1::{api::*, helpers::get_validation_schema}; +use v1::{api::*, helpers::get_default_config_validation_schema}; #[actix_web::main] async fn main() -> Result<()> { @@ -39,7 +39,7 @@ async fn main() -> Result<()> { .app_data(Data::new(AppState { db: db_addr.clone(), db_pool: pool.clone(), - default_config_validation_schema: get_validation_schema(), + default_config_validation_schema: get_default_config_validation_schema(), })) .wrap(logger) .route( diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index 0769d64f2..ea38fd114 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -1,7 +1,7 @@ -use std::{env::VarError, str::FromStr, fmt}; +use std::{env::VarError, fmt, str::FromStr}; -use actix_web::{Error, error::ErrorInternalServerError}; -use jsonschema::{JSONSchema, Draft}; +use actix_web::{error::ErrorInternalServerError, Error}; +use jsonschema::{Draft, JSONSchema}; use serde_json::json; //WARN Do NOT use this fxn inside api requests, instead add the required @@ -23,11 +23,10 @@ where pub trait ToActixErr { fn map_err_to_internal_server(self, log_prefix: &str, err_body: B) -> Result where - B: fmt::Debug + fmt::Display + 'static, - ; + B: fmt::Debug + fmt::Display + 'static; } -impl ToActixErr for Result +impl ToActixErr for Result where E: fmt::Debug, { @@ -42,36 +41,36 @@ where } } -pub fn get_validation_schema() -> JSONSchema { +pub fn get_default_config_validation_schema() -> JSONSchema { let my_schema = json!({ - "type": "object", - "properties": { - "type": { - "enum": ["string", "object", "enum", "number", "boolean", "array"] - } - }, - "required": ["type"], - "allOf": [ - { - "if": { - "properties": { "type": { "const": "object" } } - }, - "then": { - "properties": { "properties": { "type": "object" } }, - "required": ["properties"] - } - }, - { - "if": { - "properties": { "type": { "const": "string" } } - }, - "then": { - "properties": { "pattern": { "type": "string" } }, - "required": ["pattern"] - } - }, - // TODO: Add validations for Array types. - ] + "type": "object", + "properties": { + "type": { + "enum": ["string", "object", "enum", "number", "boolean", "array"] + } + }, + "required": ["type"], + "allOf": [ +// { +// "if": { +// "properties": { "type": { "const": "object" } } +// }, +// "then": { +// "properties": { "properties": { "type": "object" } }, +// "required": ["properties"] +// } +// }, + { + "if": { + "properties": { "type": { "const": "string" } } + }, + "then": { + "properties": { "pattern": { "type": "string" } }, + "required": ["pattern"] + } + }, + // TODO: Add validations for Array types. + ] }); JSONSchema::options() From d31cb14c1b588e4c78c04c136c06e463768cfef1 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 7 Jul 2023 18:08:38 +0530 Subject: [PATCH 038/352] fix: removed unecessary and wrap over conditions --- src/v1/api/context/handlers.rs | 7 ++----- src/v1/api/context/types.rs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index d2c50c5b4..f024a5a95 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -20,7 +20,7 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::{json, Value, Value::Null}; +use serde_json::{Value, Value::Null}; pub fn endpoints() -> Scope { Scope::new("") @@ -63,10 +63,7 @@ async fn add_contexts_overrides( req: web::Json, state: Data, ) -> HttpResponse { - let ctxt_cond = json!({ - "and": req.context - }); - + let ctxt_cond = Value::Object(req.context.to_owned()); let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { diff --git a/src/v1/api/context/types.rs b/src/v1/api/context/types.rs index 7c10e8f0a..88df859d9 100644 --- a/src/v1/api/context/types.rs +++ b/src/v1/api/context/types.rs @@ -3,7 +3,7 @@ use serde_json::{ Value, Map}; #[derive(Deserialize)] pub struct AddContextReq { - pub context: Vec>, + pub context: Map, pub r#override: Value, } From 7120d611d7102199300df2aadd635e03bccf8a64 Mon Sep 17 00:00:00 2001 From: Shrey Bana Date: Fri, 7 Jul 2023 18:04:53 +0530 Subject: [PATCH 039/352] feat: Added authentication. --- .env.example | 1 + .rustfmt.toml | 1 + Cargo.toml | 2 +- src/db/utils.rs | 8 +++-- src/main.rs | 4 ++- src/v1/api/context/handlers.rs | 22 ++++++++++--- src/v1/api/mod.rs | 1 + src/v1/api/types.rs | 59 ++++++++++++++++++++++++++++++++++ src/v1/db/schema.rs | 6 +++- 9 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 src/v1/api/types.rs diff --git a/.env.example b/.env.example index 12780bedb..6fea85dad 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,4 @@ DB_USER=postgres DB_HOST=localhost:5432 DB_NAME=config APP_ENV=DEV +ADMIN_TOKEN="abcd-1234" \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml index 1a076d889..e9ea2dcd3 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1 +1,2 @@ edition="2018" +max_width=90 diff --git a/Cargo.toml b/Cargo.toml index e9c8c4ca0..988aa1e8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,4 @@ base64 = "0.21.2" diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } urlencoding = "~2.1.2" jsonschema = "~0.17" -reqwest = { version = "*", features = [ "rustls-tls" ] } \ No newline at end of file +reqwest = { version = "*", features = [ "rustls-tls" ] } diff --git a/src/db/utils.rs b/src/db/utils.rs index 735e6f2bd..992f78783 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -3,8 +3,8 @@ use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; -use urlencoding::encode; use jsonschema::JSONSchema; +use urlencoding::encode; use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; @@ -12,7 +12,8 @@ use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; pub struct AppState { pub db: Addr, pub db_pool: Pool>, - pub default_config_validation_schema: JSONSchema + pub default_config_validation_schema: JSONSchema, + pub admin_token: String, } pub struct DbActor(pub Pool>); @@ -23,7 +24,8 @@ impl Actor for DbActor { pub async fn get_pool() -> Pool> { let db_url = get_database_url().await; - let manager: ConnectionManager = ConnectionManager::::new(db_url); + let manager: ConnectionManager = + ConnectionManager::::new(db_url); Pool::builder() .build(manager) .expect("Error building a connection pool") diff --git a/src/main.rs b/src/main.rs index 3188aec80..7d741aca2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use api::derived::{ }; use dotenv; -use std::io::Result; +use std::{env, io::Result}; use actix::SyncArbiter; use actix_web::{ @@ -33,6 +33,7 @@ async fn main() -> Result<()> { let pool = get_pool().await; let pool_cl = pool.clone(); let db_addr = SyncArbiter::start(5, move || DbActor(pool_cl.clone())); + let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); HttpServer::new(move || { let logger: Logger = Logger::default(); App::new() @@ -40,6 +41,7 @@ async fn main() -> Result<()> { db: db_addr.clone(), db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), + admin_token: admin_token.to_owned() })) .wrap(logger) .route( diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index f024a5a95..08cda4e9a 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -1,7 +1,10 @@ use crate::{ db::utils::AppState, v1::{ - api::context::types::{AddContextReq, AddContextResp, PaginationParams}, + api::{ + context::types::{AddContextReq, AddContextResp, PaginationParams}, + types::AuthenticationInfo, + }, db::{ models::{Context, Dimension}, schema::{contexts::dsl::contexts, dimensions::dsl::dimensions}, @@ -31,7 +34,10 @@ pub fn endpoints() -> Scope { type DBConnection = PooledConnection>; -fn val_dimensions_cal_priority(conn: &mut DBConnection, cond: &Value) -> Result { +fn val_dimensions_cal_priority( + conn: &mut DBConnection, + cond: &Value, +) -> Result { let mut get_priority = |key: &String, val: &Value| -> Result { if key == "var" { let dimension_name = val @@ -62,6 +68,7 @@ fn val_dimensions_cal_priority(conn: &mut DBConnection, cond: &Value) -> Result< async fn add_contexts_overrides( req: web::Json, state: Data, + auth_info: AuthenticationInfo, ) -> HttpResponse { let ctxt_cond = Value::Object(req.context.to_owned()); let mut conn = match state.db_pool.get() { @@ -84,6 +91,7 @@ async fn add_contexts_overrides( let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); + let AuthenticationInfo(email) = auth_info; let new_ctxt = Context { id: context_id.clone(), value: ctxt_cond, @@ -91,7 +99,7 @@ async fn add_contexts_overrides( override_id: override_id.to_owned(), override_: req.r#override.to_owned(), created_at: Utc::now(), - created_by: "some_user".to_string(), + created_by: email, }; let insert = diesel::insert_into(contexts) @@ -119,7 +127,10 @@ async fn add_contexts_overrides( } #[get("/{ctx_id}")] -async fn get_context(path: web::Path, state: Data) -> Result { +async fn get_context( + path: web::Path, + state: Data, +) -> Result { use crate::v1::db::schema::contexts::dsl::*; let ctx_id = path.into_inner(); @@ -131,7 +142,8 @@ async fn get_context(path: web::Path, state: Data) -> Result> = contexts.filter(id.eq(ctx_id)).load(&mut conn); + let result: QueryResult> = + contexts.filter(id.eq(ctx_id)).load(&mut conn); let ctx_vec = match result { Ok(ctx_vec) => ctx_vec, diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs index 654a075a0..98aaf28ad 100644 --- a/src/v1/api/mod.rs +++ b/src/v1/api/mod.rs @@ -2,3 +2,4 @@ pub mod context; pub mod dimension; pub mod default_config; pub mod config; +pub mod types; diff --git a/src/v1/api/types.rs b/src/v1/api/types.rs new file mode 100644 index 000000000..0c9c69c67 --- /dev/null +++ b/src/v1/api/types.rs @@ -0,0 +1,59 @@ +use std::{ + future::{ready, Ready}, + println, +}; + +use actix_web::{error, web::Data, Error, FromRequest}; + +use crate::db::utils::AppState; + +#[derive(Clone)] +pub struct AuthenticationInfo(pub String); +impl FromRequest for AuthenticationInfo { + type Error = Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let opt_token = req + .headers() + .get("Authorization") + .and_then(|h| h.to_str().ok()) + .and_then(|h| { + if h.starts_with("Bearer") { + Some(h) + } else { + None + } + }) + .and_then(|h| { + h.split(' ') + .collect::>() + .get(1) + .map(|token| token.to_string()) + }); + dbg!(format!("Token is \"{:?}\"", opt_token)); + let opt_admin_token = req + .app_data() + .map(|d: &Data| d.admin_token.as_str()); + + let result = match (opt_token, opt_admin_token) { + (_, None) => { + println!("ERROR: ADMIN TOKEN NOT FOUND!!!!"); + Err(error::ErrorInternalServerError("")) + } + (None, _) => Err(error::ErrorUnauthorized("Bearer token required.")), + (Some(token), Some(admin_token)) if token != admin_token => { + Err(error::ErrorUnauthorized("")) + } + (Some(_token), Some(_admin_token)) => { + let email = "cac.admin@juspay.in"; + let auth_info = AuthenticationInfo(email.to_string()); + Ok(auth_info) + } + }; + ready(result) + } +} diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index c8d205c09..bf3c32467 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -43,4 +43,8 @@ diesel::table! { } } -diesel::allow_tables_to_appear_in_same_query!(contexts, default_configs, dimensions,); +diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, +); From 53ae341d7cd89b7982b6ba71ec9bdeb5bb612922 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 17 Jul 2023 18:50:28 +0530 Subject: [PATCH 040/352] feat: added DELETE /context/{ctx_id} api --- src/v1/api/context/handlers.rs | 32 ++++++++++++++++++++++++++++++-- src/v1/db/models.rs | 3 ++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 08cda4e9a..5bca122eb 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -7,18 +7,21 @@ use crate::{ }, db::{ models::{Context, Dimension}, - schema::{contexts::dsl::contexts, dimensions::dsl::dimensions}, + schema::{contexts, dimensions::dsl::dimensions}, }, helpers::ToActixErr, }, }; use actix_web::{ - error, get, put, + delete, + error::{self, ErrorInternalServerError, ErrorNotFound}, + get, put, web::{self, Data}, HttpResponse, Responder, Result, Scope, }; use chrono::Utc; use diesel::{ + delete, r2d2::{ConnectionManager, PooledConnection}, result::{DatabaseErrorKind::*, Error::DatabaseError}, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, @@ -30,6 +33,7 @@ pub fn endpoints() -> Scope { .service(add_contexts_overrides) .service(get_context) .service(list_contexts) + .service(delete_context) } type DBConnection = PooledConnection>; @@ -70,6 +74,7 @@ async fn add_contexts_overrides( state: Data, auth_info: AuthenticationInfo, ) -> HttpResponse { + use contexts::dsl::contexts; let ctxt_cond = Value::Object(req.context.to_owned()); let mut conn = match state.db_pool.get() { Ok(conn) => conn, @@ -194,3 +199,26 @@ async fn list_contexts( .map_err_to_internal_server("Failed to execute query, error", Null)?; Ok(web::Json(result)) } + +#[delete("/{ctx_id}")] +async fn delete_context( + state: Data, + path: web::Path, +) -> actix_web::Result { + use contexts::dsl; + + let mut conn = state + .db_pool + .get() + .map_err_to_internal_server("Unable to get db connection from pool", Null)?; + let ctx_id = path.into_inner(); + let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(ctx_id))).execute(&mut conn); + match deleted_row { + Ok(0) => Err(ErrorNotFound("")), + Ok(_) => Ok(HttpResponse::NoContent().finish()), + Err(e) => { + log::error!("context delete query failed with error: {e}"); + Err(ErrorInternalServerError("")) + } + } +} diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 9a11815c2..e01ecf914 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -5,7 +5,7 @@ use diesel::{AsChangeset, Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Queryable, Selectable, Insertable, Clone, Serialize)] +#[derive(Queryable, Selectable, Insertable, Clone, Serialize, Debug)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(id))] pub struct Context { @@ -15,6 +15,7 @@ pub struct Context { pub created_at: DateTime, pub created_by: String, pub priority: i32, + #[serde(rename(serialize = "override"))] pub override_: Value, } From b649c9db15d0737876e0eef99037d9ba5b2836d6 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 19 Jul 2023 18:38:44 +0530 Subject: [PATCH 041/352] fix: added search path for schema in database_url --- src/db/utils.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/db/utils.rs b/src/db/utils.rs index 992f78783..ec5537acc 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -38,5 +38,16 @@ async fn get_database_url() -> String { let db_password = encode(db_password_raw.as_str()).to_string(); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); - format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") + let options_search_path = String::from("?options=--search_path%3d"); + let db_schema = match ( + get_from_env_unsafe::("DB_SCHEMA"), + get_from_env_unsafe::("APP_ENV"), + ) { + (Ok(schema_name), _) => format!("{options_search_path}{schema_name}"), + (Err(_), Ok(env)) if env.as_str() == "PROD" => { + format!("{options_search_path}cac_v1") + } + _ => String::from(""), + }; + format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}{db_schema}") } From 0c4fa3e318d7256c29f7da6ea0ea939d7410c9d1 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 19 Jul 2023 19:34:40 +0530 Subject: [PATCH 042/352] fix: database schema url --- src/db/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/utils.rs b/src/db/utils.rs index ec5537acc..e4a098967 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -38,7 +38,7 @@ async fn get_database_url() -> String { let db_password = encode(db_password_raw.as_str()).to_string(); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); - let options_search_path = String::from("?options=--search_path%3d"); + let options_search_path = String::from("?options=-c search_path%3D"); let db_schema = match ( get_from_env_unsafe::("DB_SCHEMA"), get_from_env_unsafe::("APP_ENV"), From 3802d7f7ea7089ffe1cfc409cb15cb2da017da1a Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 19 Jul 2023 19:57:30 +0530 Subject: [PATCH 043/352] fix: added moved tables to cac_v1 schema --- diesel.toml | 1 + .../down.sql | 9 ++ .../up.sql | 10 +++ src/db/mod.rs | 2 +- src/db/utils.rs | 13 +-- src/v1/api/config/handlers.rs | 2 +- src/v1/api/context/handlers.rs | 6 +- src/v1/api/default_config/handlers.rs | 2 +- src/v1/api/dimension/handlers.rs | 2 +- src/v1/db/models.rs | 3 +- src/v1/db/schema.rs | 83 ++++++++++--------- 11 files changed, 72 insertions(+), 61 deletions(-) create mode 100644 migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql create mode 100644 migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql diff --git a/diesel.toml b/diesel.toml index 83a56ea8d..c4c7de248 100644 --- a/diesel.toml +++ b/diesel.toml @@ -1,5 +1,6 @@ [print_schema] file = "src/v1/db/schema.rs" +schema = "cac_v1" [migrations_directory] dir = "migrations/v1" diff --git a/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql b/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql new file mode 100644 index 000000000..a0881c043 --- /dev/null +++ b/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql @@ -0,0 +1,9 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE cac_v1.contexts + SET SCHEMA public; + +ALTER TABLE cac_v1.default_configs + SET SCHEMA public; + +ALTER TABLE cac_v1.dimensions + SET SCHEMA public; diff --git a/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql b/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql new file mode 100644 index 000000000..3c8bba58e --- /dev/null +++ b/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql @@ -0,0 +1,10 @@ +CREATE SCHEMA IF NOT EXISTS cac_v1; + +ALTER TABLE contexts + SET SCHEMA cac_v1; + +ALTER TABLE default_configs + SET SCHEMA cac_v1; + +ALTER TABLE dimensions + SET SCHEMA cac_v1; diff --git a/src/db/mod.rs b/src/db/mod.rs index 81be08b94..347009280 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,5 @@ pub mod handlers; pub mod messages; pub mod models; -pub mod schema; pub mod utils; +pub mod schema; diff --git a/src/db/utils.rs b/src/db/utils.rs index e4a098967..992f78783 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -38,16 +38,5 @@ async fn get_database_url() -> String { let db_password = encode(db_password_raw.as_str()).to_string(); let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); - let options_search_path = String::from("?options=-c search_path%3D"); - let db_schema = match ( - get_from_env_unsafe::("DB_SCHEMA"), - get_from_env_unsafe::("APP_ENV"), - ) { - (Ok(schema_name), _) => format!("{options_search_path}{schema_name}"), - (Err(_), Ok(env)) if env.as_str() == "PROD" => { - format!("{options_search_path}cac_v1") - } - _ => String::from(""), - }; - format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}{db_schema}") + format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") } diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index 9c3a726fa..199aed0cf 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -2,7 +2,7 @@ use super::types::Config; use crate::{ db::utils::AppState, v1::{ - db::schema::{contexts::dsl as ctxt, default_configs::dsl as def_conf}, + db::schema::cac_v1::{contexts::dsl as ctxt, default_configs::dsl as def_conf}, helpers::ToActixErr, }, }; diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 5bca122eb..0f87bd09b 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -7,7 +7,7 @@ use crate::{ }, db::{ models::{Context, Dimension}, - schema::{contexts, dimensions::dsl::dimensions}, + schema::cac_v1::{contexts, dimensions::dsl::dimensions}, }, helpers::ToActixErr, }, @@ -136,7 +136,7 @@ async fn get_context( path: web::Path, state: Data, ) -> Result { - use crate::v1::db::schema::contexts::dsl::*; + use crate::v1::db::schema::cac_v1::contexts::dsl::*; let ctx_id = path.into_inner(); let mut conn = match state.db_pool.get() { @@ -169,7 +169,7 @@ async fn list_contexts( qparams: web::Query, state: Data, ) -> Result { - use crate::v1::db::schema::contexts::dsl::*; + use crate::v1::db::schema::cac_v1::contexts::dsl::*; let mut conn = state .db_pool diff --git a/src/v1/api/default_config/handlers.rs b/src/v1/api/default_config/handlers.rs index 76693e59d..dcb77895e 100644 --- a/src/v1/api/default_config/handlers.rs +++ b/src/v1/api/default_config/handlers.rs @@ -2,7 +2,7 @@ use super::helpers::validate_schema; use super::types::CreateReq; use crate::{ db::utils::AppState, - v1::db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}, + v1::db::{models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs}, }; use actix_web::{ put, diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs index 9502d929a..c576ad911 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/src/v1/api/dimension/handlers.rs @@ -2,7 +2,7 @@ use crate::{ db::utils::AppState, v1::{ api::dimension::types::CreateReq, - db::{models::Dimension, schema::dimensions::dsl::*}, + db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, }, }; use actix_web::{ diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index e01ecf914..8954e58f2 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -1,4 +1,4 @@ -use crate::v1::db::schema::*; +use crate::v1::db::schema::cac_v1::*; use chrono::offset::Utc; use chrono::DateTime; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; @@ -20,6 +20,7 @@ pub struct Context { } #[derive(Debug, Clone, Copy, diesel_derive_enum::DbEnum, Deserialize, Serialize)] +#[ExistingTypePath = "crate::v1::db::schema::cac_v1::sql_types::DimensionType"] #[DbValueStyle = "UPPERCASE"] #[ExistingTypePath = "crate::v1::db::schema::sql_types::DimensionType"] pub enum DimensionType { diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index bf3c32467..1a8ad6a35 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -1,50 +1,51 @@ // @generated automatically by Diesel CLI. +pub mod cac_v1 { + pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "dimension_type"))] + pub struct DimensionType; + } -pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "dimension_type"))] - pub struct DimensionType; -} - -diesel::table! { - contexts (id) { - id -> Varchar, - value -> Json, - override_id -> Varchar, - created_at -> Timestamptz, - created_by -> Varchar, - priority -> Int4, - #[sql_name = "override"] - override_ -> Json, + diesel::table! { + cac_v1.contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + priority -> Int4, + #[sql_name = "override"] + override_ -> Json, + } } -} -diesel::table! { - default_configs (key) { - key -> Varchar, - value -> Json, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, + diesel::table! { + cac_v1.default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } } -} -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::DimensionType; + diesel::table! { + use diesel::sql_types::*; + use super::sql_types::DimensionType; - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - #[sql_name = "type"] - type_ -> DimensionType, - created_at -> Timestamptz, - created_by -> Varchar, + cac_v1.dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + #[sql_name = "type"] + type_ -> DimensionType, + created_at -> Timestamptz, + created_by -> Varchar, + } } -} -diesel::allow_tables_to_appear_in_same_query!( - contexts, - default_configs, - dimensions, -); + diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, + ); +} From 141455be84e834946204ccd842e7fdedf9fd1523 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 18 Jul 2023 20:02:46 +0530 Subject: [PATCH 044/352] fix: enabled override updates in PUT /context by deep_merge --- Cargo.lock | 24 ++++++- Cargo.toml | 1 + src/v1/api/context/handlers.rs | 116 +++++++++++++++++++++------------ src/v1/api/context/types.rs | 4 +- src/v1/db/models.rs | 2 +- 5 files changed, 101 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3566cf1c4..a14c5c31a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "diesel-derive-enum", "dotenv", "env_logger", + "json-patch", "jsonschema", "log", "reqwest", @@ -786,7 +787,7 @@ dependencies = [ "pq-sys", "r2d2", "serde_json", - "uuid 0.8.2", + "uuid 1.3.4", ] [[package]] @@ -1369,6 +1370,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + [[package]] name = "jsonschema" version = "0.17.0" @@ -2496,6 +2509,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 988aa1e8b..0f352efa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,4 @@ diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } urlencoding = "~2.1.2" jsonschema = "~0.17" reqwest = { version = "*", features = [ "rustls-tls" ] } +json-patch = "1.0.0" diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 0f87bd09b..e156b554d 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -2,7 +2,7 @@ use crate::{ db::utils::AppState, v1::{ api::{ - context::types::{AddContextReq, AddContextResp, PaginationParams}, + context::types::{PaginationParams, PutReq, PutResp}, types::AuthenticationInfo, }, db::{ @@ -14,9 +14,9 @@ use crate::{ }; use actix_web::{ delete, - error::{self, ErrorInternalServerError, ErrorNotFound}, + error::{self, ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, get, put, - web::{self, Data}, + web::{self, Data, Path}, HttpResponse, Responder, Result, Scope, }; use chrono::Utc; @@ -30,7 +30,7 @@ use serde_json::{Value, Value::Null}; pub fn endpoints() -> Scope { Scope::new("") - .service(add_contexts_overrides) + .service(put_context) .service(get_context) .service(list_contexts) .service(delete_context) @@ -68,65 +68,97 @@ fn val_dimensions_cal_priority( } } -#[put("add")] -async fn add_contexts_overrides( - req: web::Json, - state: Data, +fn create_ctx_from_put_req( + req: web::Json, + conn: &mut DBConnection, auth_info: AuthenticationInfo, -) -> HttpResponse { - use contexts::dsl::contexts; - let ctxt_cond = Value::Object(req.context.to_owned()); - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - println!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - }; - - let priority = match val_dimensions_cal_priority(&mut conn, &ctxt_cond) { +) -> actix_web::Result { + let ctx_condition = Value::Object(req.context.to_owned()); + let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { Ok(0) => { - return HttpResponse::BadRequest().body("No dimension found in contexts"); + return Err(ErrorBadRequest("No dimension found in context")); } Err(e) => { - return HttpResponse::BadRequest().body(e); + return Err(ErrorBadRequest(e)); } Ok(p) => p, }; - let context_id = blake3::hash((ctxt_cond).to_string().as_bytes()).to_string(); + let context_id = blake3::hash((ctx_condition).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); let AuthenticationInfo(email) = auth_info; - let new_ctxt = Context { + Ok(Context { id: context_id.clone(), - value: ctxt_cond, + value: ctx_condition, priority, override_id: override_id.to_owned(), override_: req.r#override.to_owned(), created_at: Utc::now(), created_by: email, + }) +} + +fn update_override_of_existing_ctx( + conn: &mut PgConnection, + ctx: Context, +) -> Result, diesel::result::Error> { + use contexts::dsl; + let mut new_override: Value = dsl::contexts + .filter(dsl::id.eq(&ctx.id)) + .select(dsl::override_) + .first(conn)?; + json_patch::merge(&mut new_override, &ctx.override_); + let new_override_id = blake3::hash((new_override).to_string().as_bytes()).to_string(); + let new_ctx = Context { + override_: new_override, + override_id: new_override_id, + ..ctx }; + diesel::update(dsl::contexts) + .filter(dsl::id.eq(&new_ctx.id)) + .set(&new_ctx) + .execute(conn)?; + Ok(web::Json(get_put_resp(new_ctx))) +} + +fn get_put_resp(ctx: Context) -> PutResp { + PutResp { + context_id: ctx.id, + override_id: ctx.override_id, + priority: ctx.priority, + } +} + +#[put("")] +async fn put_context( + req: web::Json, + state: Data, + auth_info: AuthenticationInfo, +) -> actix_web::Result> { + use contexts::dsl::contexts; + let mut conn = state + .db_pool + .get() + .map_err_to_internal_server("unable to get db connection from pool", "")?; + + let new_ctx = create_ctx_from_put_req(req, &mut conn, auth_info)?; let insert = diesel::insert_into(contexts) - .values(&new_ctxt) + .values(&new_ctx) .execute(&mut conn); - let resp = AddContextResp { - context_id, - override_id, - priority, - }; - match insert { - Ok(_) => HttpResponse::Created() - .insert_header(("x-info", "new context created")) - .json(resp), - Err(DatabaseError(UniqueViolation, _)) => HttpResponse::Ok() - .insert_header(("x-info", "context already exists")) - .json(resp), + Ok(_) => Ok(web::Json(get_put_resp(new_ctx))), + Err(DatabaseError(UniqueViolation, _)) => { + update_override_of_existing_ctx(&mut conn, new_ctx) + .map_err_to_internal_server( + "override update of existing context failed", + "", + ) + } e => { - println!("DB transaction failed with error: {e:?}"); - return HttpResponse::InternalServerError().finish(); + println!("context insert failed with error: {e:?}"); + Err(ErrorInternalServerError("")) } } } @@ -203,14 +235,14 @@ async fn list_contexts( #[delete("/{ctx_id}")] async fn delete_context( state: Data, - path: web::Path, + path: Path, ) -> actix_web::Result { use contexts::dsl; let mut conn = state .db_pool .get() - .map_err_to_internal_server("Unable to get db connection from pool", Null)?; + .map_err_to_internal_server("Unable to get db connection from pool", "")?; let ctx_id = path.into_inner(); let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(ctx_id))).execute(&mut conn); match deleted_row { diff --git a/src/v1/api/context/types.rs b/src/v1/api/context/types.rs index 88df859d9..d3dad705d 100644 --- a/src/v1/api/context/types.rs +++ b/src/v1/api/context/types.rs @@ -2,13 +2,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{ Value, Map}; #[derive(Deserialize)] -pub struct AddContextReq { +pub struct PutReq { pub context: Map, pub r#override: Value, } #[derive(Serialize)] -pub struct AddContextResp { +pub struct PutResp { pub context_id: String, pub override_id: String, pub priority: i32, diff --git a/src/v1/db/models.rs b/src/v1/db/models.rs index 8954e58f2..d78a8b213 100644 --- a/src/v1/db/models.rs +++ b/src/v1/db/models.rs @@ -5,7 +5,7 @@ use diesel::{AsChangeset, Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Queryable, Selectable, Insertable, Clone, Serialize, Debug)] +#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Serialize, Debug)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(id))] pub struct Context { From c838b96e3d42ae17ed8dae97b092f1c0bbcc4c71 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 18 Jul 2023 20:45:27 +0530 Subject: [PATCH 045/352] feat: PUT /context/move/{ctx_id} api - given a ctx_id update it's context as well as overrides to the input one --- src/v1/api/context/handlers.rs | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index e156b554d..6a881dfdc 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -34,6 +34,7 @@ pub fn endpoints() -> Scope { .service(get_context) .service(list_contexts) .service(delete_context) + .service(move_context) } type DBConnection = PooledConnection>; @@ -163,6 +164,53 @@ async fn put_context( } } +#[put("/move/{ctx_id}")] +async fn move_context( + state: Data, + path: Path, + req: web::Json, + auth_info: AuthenticationInfo, +) -> actix_web::Result> { + use contexts::dsl; + let conn = &mut state + .db_pool + .get() + .map_err_to_internal_server("unable to get db connection from pool", "")?; + let new_ctx = create_ctx_from_put_req(req, conn, auth_info)?; + let old_ctx_id = path.into_inner(); + let update = diesel::update(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + //NOTE we have to specifically set id because + //diesel's #derive(AsChangeset) by default + //assumes that we want to ignore it + .set((&new_ctx, dsl::id.eq(&new_ctx.id))) + .execute(conn); + + let handle_unique_violation = |db_conn: &mut DBConnection, new_ctx: Context| { + db_conn + .build_transaction() + .read_write() + .run(|conn| { + diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .execute(conn)?; + update_override_of_existing_ctx(conn, new_ctx) + }) + .map_err_to_internal_server("update query failed", "") + }; + match update { + Ok(0) => Err(ErrorNotFound(format!( + "context with id: {old_ctx_id} not found" + ))), + Ok(_) => Ok(web::Json(get_put_resp(new_ctx))), + Err(DatabaseError(UniqueViolation, _)) => handle_unique_violation(conn, new_ctx), + Err(e) => { + log::error!("update query failed with error: {e:?}"); + Err(ErrorInternalServerError("")) + } + } +} + #[get("/{ctx_id}")] async fn get_context( path: web::Path, From f62beaf1417e255f1219971eaa013c218d83cb14 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 21 Jul 2023 17:31:53 +0530 Subject: [PATCH 046/352] fix: moved dimension_type to cac_v1 schema --- .../down.sql | 3 +++ .../up.sql | 2 ++ src/v1/db/schema.rs | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql create mode 100644 migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql diff --git a/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql b/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql new file mode 100644 index 000000000..f86859121 --- /dev/null +++ b/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TYPE dimension_type + SET SCHEMA public; diff --git a/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql b/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql new file mode 100644 index 000000000..b3ed2962c --- /dev/null +++ b/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql @@ -0,0 +1,2 @@ +ALTER TYPE dimension_type + SET SCHEMA cac_v1; diff --git a/src/v1/db/schema.rs b/src/v1/db/schema.rs index 1a8ad6a35..992f759d9 100644 --- a/src/v1/db/schema.rs +++ b/src/v1/db/schema.rs @@ -1,8 +1,9 @@ // @generated automatically by Diesel CLI. + pub mod cac_v1 { pub mod sql_types { #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "dimension_type"))] + #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] pub struct DimensionType; } From c26ca88e27fe1b9fd4cffb51718b7de08c22e92b Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Tue, 25 Jul 2023 11:55:01 +0530 Subject: [PATCH 047/352] added context_id in GET/config response --- src/v1/api/config/handlers.rs | 20 +++++++++++--------- src/v1/api/config/types.rs | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index 199aed0cf..d04a9bfed 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -26,15 +26,16 @@ async fn get(state: Data) -> actix_web::Result> { .map_err_to_internal_server("error getting a connection from db pool", Null)?; let contexts_vec = ctxt::contexts - .select((ctxt::value, ctxt::override_id, ctxt::override_)) + .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) .order_by(ctxt::priority.asc()) - .load::<(Value, String, Value)>(&mut conn) + .load::<(String, Value, String, Value)>(&mut conn) .map_err_to_internal_server("error getting contexts", Null)?; let (contexts, overrides) = contexts_vec.into_iter().fold( (Vec::new(), Map::new()), - |(mut ctxts, mut overrides), (condition, override_id, override_)| { + |(mut ctxts, mut overrides), (context_id, condition, override_id, override_)| { let ctxt = super::types::Context { + context_id: context_id, condition, override_with_keys: [override_id.to_owned()], }; @@ -49,12 +50,13 @@ async fn get(state: Data) -> actix_web::Result> { .load::<(String, Value)>(&mut conn) .map_err_to_internal_server("error getting default configs", Null)?; - let default_configs = default_config_vec - .into_iter() - .fold(Map::new(), |mut acc, item| { - acc.insert(item.0, item.1); - acc - }); + let default_configs = + default_config_vec + .into_iter() + .fold(Map::new(), |mut acc, item| { + acc.insert(item.0, item.1); + acc + }); let config = Config { contexts, diff --git a/src/v1/api/config/types.rs b/src/v1/api/config/types.rs index f27d4b073..0ae119f67 100644 --- a/src/v1/api/config/types.rs +++ b/src/v1/api/config/types.rs @@ -1,16 +1,16 @@ use serde::Serialize; -use serde_json::{ Map, Value}; +use serde_json::{Map, Value}; #[derive(Serialize)] pub struct Config { pub contexts: Vec, pub overrides: Map, - pub default_configs: Map + pub default_configs: Map, } #[derive(Serialize)] pub struct Context { + pub context_id: String, pub condition: Value, pub override_with_keys: [String; 1], } - From 9d192ce3eed6d4f1187b1535e2bf9d605ad316c3 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Mon, 24 Jul 2023 19:34:30 +0530 Subject: [PATCH 048/352] added authorization todefault_configs and dimensions --- src/v1/api/context/handlers.rs | 11 +++++++++-- src/v1/api/default_config/handlers.rs | 26 ++++++++++++++++++++------ src/v1/api/dimension/handlers.rs | 23 +++++++++++++++++------ 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 6a881dfdc..818b7683c 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -26,6 +26,7 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; +use log::info; use serde_json::{Value, Value::Null}; pub fn endpoints() -> Scope { @@ -284,6 +285,7 @@ async fn list_contexts( async fn delete_context( state: Data, path: Path, + auth_info: AuthenticationInfo, ) -> actix_web::Result { use contexts::dsl; @@ -292,10 +294,15 @@ async fn delete_context( .get() .map_err_to_internal_server("Unable to get db connection from pool", "")?; let ctx_id = path.into_inner(); - let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(ctx_id))).execute(&mut conn); + let deleted_row = + delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(&mut conn); + let AuthenticationInfo(email) = auth_info; match deleted_row { Ok(0) => Err(ErrorNotFound("")), - Ok(_) => Ok(HttpResponse::NoContent().finish()), + Ok(_) => { + info!("{ctx_id} context deleted by {email}"); + Ok(HttpResponse::NoContent().finish()) + } Err(e) => { log::error!("context delete query failed with error: {e}"); Err(ErrorInternalServerError("")) diff --git a/src/v1/api/default_config/handlers.rs b/src/v1/api/default_config/handlers.rs index dcb77895e..47a4ade69 100644 --- a/src/v1/api/default_config/handlers.rs +++ b/src/v1/api/default_config/handlers.rs @@ -2,7 +2,12 @@ use super::helpers::validate_schema; use super::types::CreateReq; use crate::{ db::utils::AppState, - v1::db::{models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs}, + v1::{ + api::types::AuthenticationInfo, + db::{ + models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs, + }, + }, }; use actix_web::{ put, @@ -23,10 +28,13 @@ async fn create( state: Data, key: web::Path, request: web::Json, + auth_info: AuthenticationInfo, ) -> HttpResponse { let req = request.into_inner(); let schema = Value::Object(req.schema); - if let Err(e) = validate_schema(&state.default_config_validation_schema, schema.to_owned()) { + if let Err(e) = + validate_schema(&state.default_config_validation_schema, schema.to_owned()) + { return HttpResponse::BadRequest().body(e); }; let schema_compile_result = JSONSchema::options() @@ -44,15 +52,18 @@ async fn create( Ok(_) => (), Err(_) => { println!("Validation for value with given JSON schema failed."); - return HttpResponse::BadRequest().body("Validation with given schema failed."); + return HttpResponse::BadRequest() + .body("Validation with given schema failed."); } }; + let AuthenticationInfo(email) = auth_info; + let new_default_config = DefaultConfig { key: key.into_inner(), value: req.value, schema: schema, - created_by: String::from("some_user"), //TODO update after authentication is added + created_by: email, created_at: Utc::now(), }; @@ -69,10 +80,13 @@ async fn create( .execute(&mut conn); match upsert { - Ok(_) => return HttpResponse::Created().body("DefaultConfig created successfully."), + Ok(_) => { + return HttpResponse::Created().body("DefaultConfig created successfully.") + } Err(e) => { println!("DefaultConfig creation failed with error: {e}"); - return HttpResponse::InternalServerError().body("Failed to create DefaultConfig"); + return HttpResponse::InternalServerError() + .body("Failed to create DefaultConfig"); } } } diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs index c576ad911..fcc1b1a57 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/src/v1/api/dimension/handlers.rs @@ -1,7 +1,7 @@ use crate::{ db::utils::AppState, v1::{ - api::dimension::types::CreateReq, + api::{dimension::types::CreateReq, types::AuthenticationInfo}, db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, }, }; @@ -11,23 +11,30 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use diesel::{RunQueryDsl}; +use diesel::RunQueryDsl; pub fn endpoints() -> Scope { Scope::new("").service(create) } #[put("")] -async fn create(state: Data, req: web::Json) -> HttpResponse { +async fn create( + state: Data, + req: web::Json, + auth_info: AuthenticationInfo, +) -> HttpResponse { //TODO move this to the type itself rather than special if check if req.priority <= 0 { return HttpResponse::BadRequest().body("Priority should be greater than 0"); } + + let AuthenticationInfo(email) = auth_info; + let new_dimension = Dimension { dimension: req.dimension.clone(), priority: i32::from(req.priority), type_: req.r#type, - created_by: String::from("some_user"), //TODO update after authentication is added + created_by: email, created_at: Utc::now(), }; @@ -47,10 +54,14 @@ async fn create(state: Data, req: web::Json) -> HttpRespons .execute(&mut conn); match upsert { - Ok(_) => return HttpResponse::Created().body("Dimension created/updated successfully."), + Ok(_) => { + return HttpResponse::Created() + .body("Dimension created/updated successfully.") + } Err(e) => { println!("Dimension upsert failed with error: {e}"); - return HttpResponse::InternalServerError().body("Failed to create/udpate dimension\n"); + return HttpResponse::InternalServerError() + .body("Failed to create/udpate dimension\n"); } } } From e84811cdbcf93c5d3a1c05f57034a6f197417d50 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 25 Jul 2023 15:21:04 +0530 Subject: [PATCH 049/352] fix: fixed ordering of /context endpoints - so that endpoints like /context/list don't end up getting into /context/{ctx_id} as {ctx_id} here will act as a wildcard - also starting to do `diesel migration run` during make run --- makefile | 1 + src/v1/api/context/handlers.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/makefile b/makefile index bf851eb63..196be8134 100644 --- a/makefile +++ b/makefile @@ -27,6 +27,7 @@ run: touch ./docker-compose/localstack/export_cyphers.sh cargo build --color always docker-compose up -d postgres localstack + diesel migration run source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 818b7683c..1c34aa929 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -32,10 +32,10 @@ use serde_json::{Value, Value::Null}; pub fn endpoints() -> Scope { Scope::new("") .service(put_context) - .service(get_context) .service(list_contexts) - .service(delete_context) .service(move_context) + .service(get_context) + .service(delete_context) } type DBConnection = PooledConnection>; From 578edbe8c180df2b6a29b4f34dcd1ded0fbb2421 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Tue, 25 Jul 2023 11:55:01 +0530 Subject: [PATCH 050/352] added context_id in GET/config response --- src/v1/api/config/handlers.rs | 4 ++-- src/v1/api/config/types.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index d04a9bfed..f77076c61 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -33,9 +33,9 @@ async fn get(state: Data) -> actix_web::Result> { let (contexts, overrides) = contexts_vec.into_iter().fold( (Vec::new(), Map::new()), - |(mut ctxts, mut overrides), (context_id, condition, override_id, override_)| { + |(mut ctxts, mut overrides), (id, condition, override_id, override_)| { let ctxt = super::types::Context { - context_id: context_id, + id: id, condition, override_with_keys: [override_id.to_owned()], }; diff --git a/src/v1/api/config/types.rs b/src/v1/api/config/types.rs index 0ae119f67..98b571871 100644 --- a/src/v1/api/config/types.rs +++ b/src/v1/api/config/types.rs @@ -10,7 +10,7 @@ pub struct Config { #[derive(Serialize)] pub struct Context { - pub context_id: String, + pub id: String, pub condition: Value, pub override_with_keys: [String; 1], } From ee76ab5fef6bccf577a457622fd56add9c251782 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Tue, 25 Jul 2023 18:01:57 +0530 Subject: [PATCH 051/352] ci: automated newman test setup ci: setup testing in jenkins --- .env.example | 6 +- .gitignore | 1 + Jenkinsfile | 8 +- docker-compose.yaml | 19 +- docker-compose/postgres/Dockerfile | 2 +- makefile | 15 +- package-lock.json | 1442 +++++++++++++++++ package.json | 12 + postman/cac.postman_collection.json | 759 +++++++++ postman/cac/.event.json | 22 + postman/cac/.info.json | 7 + postman/cac/.meta.json | 8 + postman/cac/.variable.json | 14 + postman/cac/Context/.meta.json | 10 + .../cac/Context/Create Context/event.test.js | 77 + .../cac/Context/Create Context/request.json | 28 + .../cac/Context/Create Context/response.json | 1 + .../cac/Context/Delete Context/event.test.js | 26 + .../cac/Context/Delete Context/request.json | 20 + .../cac/Context/Delete Context/response.json | 1 + postman/cac/Context/Get Context/event.test.js | 29 + postman/cac/Context/Get Context/request.json | 20 + postman/cac/Context/Get Context/response.json | 1 + .../cac/Context/List Context/event.test.js | 11 + postman/cac/Context/List Context/request.json | 20 + .../cac/Context/List Context/response.json | 1 + .../cac/Context/Move Context/event.test.js | 82 + postman/cac/Context/Move Context/request.json | 30 + .../cac/Context/Move Context/response.json | 1 + .../cac/Context/Update Context/event.test.js | 77 + .../cac/Context/Update Context/request.json | 28 + .../cac/Context/Update Context/response.json | 1 + postman/cac/Default Config/.meta.json | 5 + .../Add default-config key/event.test.js | 33 + .../Add default-config key/request.json | 29 + .../Add default-config key/response.json | 1 + postman/cac/Dimension/.meta.json | 5 + .../Dimension/Create Dimension/event.test.js | 3 + .../Dimension/Create Dimension/request.json | 28 + .../Dimension/Create Dimension/response.json | 1 + postman/cac/config/.meta.json | 5 + postman/cac/config/Get Config/event.test.js | 3 + postman/cac/config/Get Config/request.json | 19 + postman/cac/config/Get Config/response.json | 1 + 44 files changed, 2888 insertions(+), 24 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postman/cac.postman_collection.json create mode 100644 postman/cac/.event.json create mode 100644 postman/cac/.info.json create mode 100644 postman/cac/.meta.json create mode 100644 postman/cac/.variable.json create mode 100644 postman/cac/Context/.meta.json create mode 100644 postman/cac/Context/Create Context/event.test.js create mode 100644 postman/cac/Context/Create Context/request.json create mode 100644 postman/cac/Context/Create Context/response.json create mode 100644 postman/cac/Context/Delete Context/event.test.js create mode 100644 postman/cac/Context/Delete Context/request.json create mode 100644 postman/cac/Context/Delete Context/response.json create mode 100644 postman/cac/Context/Get Context/event.test.js create mode 100644 postman/cac/Context/Get Context/request.json create mode 100644 postman/cac/Context/Get Context/response.json create mode 100644 postman/cac/Context/List Context/event.test.js create mode 100644 postman/cac/Context/List Context/request.json create mode 100644 postman/cac/Context/List Context/response.json create mode 100644 postman/cac/Context/Move Context/event.test.js create mode 100644 postman/cac/Context/Move Context/request.json create mode 100644 postman/cac/Context/Move Context/response.json create mode 100644 postman/cac/Context/Update Context/event.test.js create mode 100644 postman/cac/Context/Update Context/request.json create mode 100644 postman/cac/Context/Update Context/response.json create mode 100644 postman/cac/Default Config/.meta.json create mode 100644 postman/cac/Default Config/Add default-config key/event.test.js create mode 100644 postman/cac/Default Config/Add default-config key/request.json create mode 100644 postman/cac/Default Config/Add default-config key/response.json create mode 100644 postman/cac/Dimension/.meta.json create mode 100644 postman/cac/Dimension/Create Dimension/event.test.js create mode 100644 postman/cac/Dimension/Create Dimension/request.json create mode 100644 postman/cac/Dimension/Create Dimension/response.json create mode 100644 postman/cac/config/.meta.json create mode 100644 postman/cac/config/Get Config/event.test.js create mode 100644 postman/cac/config/Get Config/request.json create mode 100644 postman/cac/config/Get Config/response.json diff --git a/.env.example b/.env.example index 6fea85dad..8d7db92ba 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ -DATABASE_URL=postgres://postgres:docker@localhost:5432/config?sslmode=disable +DATABASE_URL=postgres://postgres:docker@127.0.0.1:5432/config?sslmode=disable RUST_LOG=info AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_SESSION_TOKEN=test AWS_REGION=ap-south-1 DB_USER=postgres -DB_HOST=localhost:5432 +DB_HOST=127.0.0.1:5432 DB_NAME=config APP_ENV=DEV -ADMIN_TOKEN="abcd-1234" \ No newline at end of file +ADMIN_TOKEN="12345678" diff --git a/.gitignore b/.gitignore index 39d37f871..047e09de4 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ backend/.env # dev bacon.toml docker-compose/localstack/export_cyphers.sh +test_logs diff --git a/Jenkinsfile b/Jenkinsfile index 62d8ec6ae..4751f97c6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,10 +25,10 @@ pipeline { } } - stage('Test') { - when { expression { SKIP_CI == 'false' } } - steps { sh 'make ci-test' } - } +// stage('Test') { +// when { expression { SKIP_CI == 'false' } } +// steps { sh 'make ci-test' } +// } stage('Build Image') { when { diff --git a/docker-compose.yaml b/docker-compose.yaml index 037ce9d1e..acfa5691b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,5 @@ version: "3.4" + services: application: container_name: "application" @@ -15,8 +16,7 @@ services: - DATABASE_URL=postgres://postgres:docker@postgres:5432/config?sslmode=disable - DB_POOL_SIZE=10 - networks: - - library-network + network_mode: bridge tty: true ports: - "8080:8080" @@ -24,17 +24,17 @@ services: postgres: build: ./docker-compose/postgres/ container_name: context-aware-config_postgres + ports: + - "5432:5432" environment: POSTGRES_PASSWORD: "docker" POSTGRES_DB: "config" - networks: - - library-network - ports: - - "5432:5432" + restart: on-failure + network_mode: bridge localstack: build : ./docker-compose/localstack/ - container_name: context-aware-config_localstack_instance + container_name: context-aware-config_localstack ports: - "4510-4559:4510-4559" # external service port range - "4566:4566" # LocalStack Edge Proxy @@ -46,8 +46,3 @@ services: EDGE_PORT: 4566 volumes: - ./docker-compose/localstack/export_cyphers.sh:/etc/localstack/export_cyphers.sh - -networks: - library-network: - driver: bridge - diff --git a/docker-compose/postgres/Dockerfile b/docker-compose/postgres/Dockerfile index a6707ff39..017c4de96 100644 --- a/docker-compose/postgres/Dockerfile +++ b/docker-compose/postgres/Dockerfile @@ -1,3 +1,3 @@ -FROM postgres:12 +FROM postgres:12-alpine COPY ./db_init.sql /docker-entrypoint-initdb.d/db_init.sql diff --git a/makefile b/makefile index 196be8134..550900255 100644 --- a/makefile +++ b/makefile @@ -6,8 +6,11 @@ build: cargo build ci-test: -## Un-comment once agent has 'cargo' & 'libpq5'. - #cargo build + npm ci --loglevel=error + -docker rm -f $$(docker container ls --filter name=^context-aware-config -q) + make run 2>&1 | tee test_logs & + while ! grep -q "starting in Actix" test_logs; do echo "waiting for bootup..." && sleep 2; done + npm run test ci-build: docker build -t $(IMAGE_NAME):$(VERSION) . @@ -25,9 +28,13 @@ registry-login: run: pkill -f target/debug/context-aware-config & touch ./docker-compose/localstack/export_cyphers.sh - cargo build --color always docker-compose up -d postgres localstack - diesel migration run + cp .env.example .env + #NOTE need to sleep here because locastack takes some time to internally + #populate the kms keyId + sleep 10 #TODO move this sleep to aws cli list-keys command instead + cargo build --color always +# diesel migration run source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..06ca68709 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1442 @@ +{ + "name": "context-aware-configuration", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "context-aware-configuration", + "version": "0.0.1", + "devDependencies": { + "newmandir": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git" + } + }, + "node_modules/@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@postman/tunnel-agent": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", + "integrity": "sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==", + "dev": true + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dev": true, + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.4.0.tgz", + "integrity": "sha512-NpwMDdSIprbYx1CLnfbxEIarI0Z+s9MssEgggMNheGM+WD68yOhV7IEA/3r6tr0yTRgQD0HuZJDw32s99i6L+A==", + "dev": true + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cli-progress": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.10.0.tgz", + "integrity": "sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/directory-tree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", + "integrity": "sha512-HqjZ49fDzUnKYUhHxVw9eKBqbQ+lL0v4kSBInlDlaktmLtGoV9tC54a6A0ZfYeIrkMHWTE6MwwmUXP477+UEKQ==", + "dev": true, + "dependencies": { + "command-line-args": "^5.2.0", + "command-line-usage": "^6.1.1" + }, + "bin": { + "directory-tree": "bin/index.js" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/http-reasons": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", + "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", + "dev": true + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/httpntlm": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.7.7.tgz", + "integrity": "sha512-Pv2Rvrz8H0qv1Dne5mAdZ9JegG1uc6Vu5lwLflIY6s8RKHdZQbW39L4dYswSgqMDT0pkJILUTKjeyU0VPNRZjA==", + "dev": true, + "dependencies": { + "httpreq": ">=0.4.22", + "underscore": "~1.12.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/httpreq": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.5.2.tgz", + "integrity": "sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw==", + "dev": true, + "engines": { + "node": ">= 6.15.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/liquid-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", + "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-format": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.1.tgz", + "integrity": "sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==", + "dev": true, + "dependencies": { + "charset": "^1.0.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/newman": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/newman/-/newman-5.3.2.tgz", + "integrity": "sha512-cWy8pV0iwvMOZLTw3hkAHcwo2ZA0GKkXm8oUMn1Ltii3ZI2nKpnrg9QGdIT0hGHChRkX6prY5e3Aar7uykMGNg==", + "dev": true, + "dependencies": { + "async": "3.2.3", + "chardet": "1.4.0", + "cli-progress": "3.10.0", + "cli-table3": "0.6.1", + "colors": "1.4.0", + "commander": "7.2.0", + "csv-parse": "4.16.3", + "eventemitter3": "4.0.7", + "filesize": "8.0.7", + "lodash": "4.17.21", + "mkdirp": "1.0.4", + "postman-collection": "4.1.1", + "postman-collection-transformer": "4.1.6", + "postman-request": "2.88.1-postman.31", + "postman-runtime": "7.29.0", + "pretty-ms": "7.0.1", + "semver": "7.3.5", + "serialised-error": "1.1.3", + "tough-cookie": "3.0.1", + "word-wrap": "1.2.3", + "xmlbuilder": "15.1.1" + }, + "bin": { + "newman": "bin/newman.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/newman/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/newmandir": { + "version": "0.0.1", + "resolved": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git#82838d888737b1c1410813cbee0b2bda2c325a5e", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "^10.0.1", + "directory-tree": "^3.5.1", + "newman": "^5.3.2" + }, + "bin": { + "newmandir": "index.js" + } + }, + "node_modules/node-oauth1": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-oauth1/-/node-oauth1-1.3.0.tgz", + "integrity": "sha512-0yggixNfrA1KcBwvh/Hy2xAS1Wfs9dcg6TdFf2zN7gilcAigMdrtZ4ybrBSXBgLvGDw9V1p2MRnGBMq7XjTWLg==", + "dev": true + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/postman-collection": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.1.1.tgz", + "integrity": "sha512-ODpJtlf8r99DMcTU7gFmi/yvQYckFzcuE6zL/fWnyrFT34ugdCBFlX+DN7M+AnP6lmR822fv5s60H4DnL4+fAg==", + "dev": true, + "dependencies": { + "faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.34", + "postman-url-encoder": "3.0.5", + "semver": "7.3.5", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection-transformer": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/postman-collection-transformer/-/postman-collection-transformer-4.1.6.tgz", + "integrity": "sha512-xvdQb6sZoWcG9xZXUPSuxocjcd6WCZlINlGGiuHdSfxhgiwQhj9qhF0JRFbagZ8xB0+pYUairD5MiCENc6DEVA==", + "dev": true, + "dependencies": { + "commander": "8.3.0", + "inherits": "2.0.4", + "lodash": "4.17.21", + "semver": "7.3.5", + "strip-json-comments": "3.1.1" + }, + "bin": { + "postman-collection-transformer": "bin/transform-collection.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection-transformer/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/postman-request": { + "version": "2.88.1-postman.31", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.31.tgz", + "integrity": "sha512-OJbYqP7ItxQ84yHyuNpDywCZB0HYbpHJisMQ9lb1cSL3N5H3Td6a2+3l/a74UMd3u82BiGC5yQyYmdOIETP/nQ==", + "dev": true, + "dependencies": { + "@postman/form-data": "~3.1.1", + "@postman/tunnel-agent": "^0.6.3", + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "brotli": "~1.3.2", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "har-validator": "~5.1.3", + "http-signature": "~1.3.1", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "stream-length": "^1.0.2", + "tough-cookie": "~2.5.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postman-request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/postman-request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/postman-runtime": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/postman-runtime/-/postman-runtime-7.29.0.tgz", + "integrity": "sha512-eXxHREE/fUpohkGPRgBY1YccSGx9cyW3mtGiPyIE4zD5fYzasgBHqW6kbEND3Xrd3yf/uht/YI1H8O7J1+A1+w==", + "dev": true, + "dependencies": { + "async": "3.2.3", + "aws4": "1.11.0", + "handlebars": "4.7.7", + "httpntlm": "1.7.7", + "js-sha512": "0.8.0", + "lodash": "4.17.21", + "mime-types": "2.1.34", + "node-oauth1": "1.3.0", + "performance-now": "2.1.0", + "postman-collection": "4.1.1", + "postman-request": "2.88.1-postman.31", + "postman-sandbox": "4.0.6", + "postman-url-encoder": "3.0.5", + "serialised-error": "1.1.3", + "tough-cookie": "3.0.1", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-runtime/node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/postman-sandbox": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postman-sandbox/-/postman-sandbox-4.0.6.tgz", + "integrity": "sha512-PPRanSNEE4zy3kO7CeSBHmAfJnGdD9ecHY/Mjh26CQuZZarGkNO8c0U/n+xX3+5M1BRNc82UYq6YCtdsSDqcng==", + "dev": true, + "dependencies": { + "lodash": "4.17.21", + "teleport-javascript": "1.0.0", + "uvm": "2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-url-encoder": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", + "integrity": "sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialised-error": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/serialised-error/-/serialised-error-1.1.3.tgz", + "integrity": "sha512-vybp3GItaR1ZtO2nxZZo8eOo7fnVaNtP3XE2vJKgzkKR2bagCkdJ1EpYYhEMd3qu/80DwQk9KjsNSxE3fXWq0g==", + "dev": true, + "dependencies": { + "object-hash": "^1.1.2", + "stack-trace": "0.0.9", + "uuid": "^3.0.0" + } + }, + "node_modules/serialised-error/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/stream-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", + "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", + "dev": true, + "dependencies": { + "bluebird": "^2.6.2" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/teleport-javascript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/teleport-javascript/-/teleport-javascript-1.0.0.tgz", + "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "dependencies": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uvm/-/uvm-2.0.2.tgz", + "integrity": "sha512-Ra+aPiS5GXAbwXmyNExqdS42sTqmmx4XWEDF8uJlsTfOkKf9Rd9xNgav1Yckv4HfVEZg4iOFODWHFYuJ+9Fzfg==", + "dev": true, + "dependencies": { + "flatted": "3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..99a1126c7 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "context-aware-configuration", + "version": "0.0.1", + "private": true, + "description": "This is just to run automated newman tests for this service", + "scripts": { + "test": "./node_modules/.bin/newmandir -n postman/cac" + }, + "devDependencies": { + "newmandir": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git" + } +} diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json new file mode 100644 index 000000000..7cf1d77ae --- /dev/null +++ b/postman/cac.postman_collection.json @@ -0,0 +1,759 @@ +{ + "collection": { + "item": [ + { + "name": "config", + "item": [ + { + "name": "Get Config", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" + ], + "path": [ + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default Config", + "item": [ + { + "name": "Add default-config key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(key, value) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const default_configs = resp_obj.default_configs;", + "", + " console.log(`Checking if key=${key} with value=${value} in default_configs`);", + " pm.expect(default_configs[key]).to.be.eq(value);", + " });", + "}", + "", + "pm.test(\"201 check\", function () {", + " pm.response.to.have.status(201);", + "})", + "", + "pm.test(\"Check if key added to default config\", function () {", + " const key = \"key1\", value = \"value1\";", + " getConfigAndTest(key, value);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"value\": \"value1\",\n \"schema\": {\n \"type\": \"string\",\n \"pattern\": \".*\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Dimension", + "item": [ + { + "name": "Create Dimension", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"201 Check\", function () {", + " pm.response.to.have.status(201);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"dimension\": \"clientId\",\n \"priority\": 100,\n \"type\": \"STRING\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Context", + "item": [ + { + "name": "Create Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value2\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Update Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Move Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", + " throw \"old context not removed on move\"", + " }", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + "", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id];", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", + " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"tamatar\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const expected_context = {", + " \"id\": pm.environment.get(\"context_id\"),", + " \"value\": {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " },", + " \"override_id\": pm.environment.get(\"override_id\"),", + " \"priority\": 100,", + " \"override\": {", + " \"key1\": \"value3\"", + " }", + "};", + "", + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Context equality check\", function() {", + " const response = pm.response.json();", + " ", + " delete response.created_at;", + " delete response.created_by;", + "", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "List Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "", + "pm.test(\"Response validation\", function() {", + " const response = pm.response.json();", + " if (response.length == 0) {", + " throw \"list context should return at least one context now\"", + " }", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "list" + ] + } + }, + "response": [] + }, + { + "name": "Delete Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "const context_id = pm.environment.get(\"context_id\");", + "", + "pm.test(\"204 check\", function() {", + " pm.response.to.have.status(204);", + "})", + "", + "pm.test(\"Fetch for context should fail with 404\", function () {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " console.log(`alloo ${error}`);", + " throw error;", + " }", + "", + " pm.expect(response.code).to.be.eq(404);", + " });", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "info": { + "_postman_id": "c0e007df-4ea8-478e-a47b-74194b94887f", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] + } +} diff --git a/postman/cac/.event.json b/postman/cac/.event.json new file mode 100644 index 000000000..761eacf8f --- /dev/null +++ b/postman/cac/.event.json @@ -0,0 +1,22 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} diff --git a/postman/cac/.info.json b/postman/cac/.info.json new file mode 100644 index 000000000..1c2a232e6 --- /dev/null +++ b/postman/cac/.info.json @@ -0,0 +1,7 @@ +{ + "info": { + "_postman_id": "c0e007df-4ea8-478e-a47b-74194b94887f", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/cac/.meta.json b/postman/cac/.meta.json new file mode 100644 index 000000000..b6ff22778 --- /dev/null +++ b/postman/cac/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Default Config", + "Dimension", + "Context", + "Config" + ] +} diff --git a/postman/cac/.variable.json b/postman/cac/.variable.json new file mode 100644 index 000000000..5460e39fb --- /dev/null +++ b/postman/cac/.variable.json @@ -0,0 +1,14 @@ +{ + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/cac/Context/.meta.json b/postman/cac/Context/.meta.json new file mode 100644 index 000000000..e947b4385 --- /dev/null +++ b/postman/cac/Context/.meta.json @@ -0,0 +1,10 @@ +{ + "childrenOrder": [ + "Create Context", + "Update Context", + "Move Context", + "Get Context", + "List Context", + "Delete Context" + ] +} diff --git a/postman/cac/Context/Create Context/event.test.js b/postman/cac/Context/Create Context/event.test.js new file mode 100644 index 000000000..ff5ca44b9 --- /dev/null +++ b/postman/cac/Context/Create Context/event.test.js @@ -0,0 +1,77 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); diff --git a/postman/cac/Context/Create Context/request.json b/postman/cac/Context/Create Context/request.json new file mode 100644 index 000000000..8d3c070bf --- /dev/null +++ b/postman/cac/Context/Create Context/request.json @@ -0,0 +1,28 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value2\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } +} diff --git a/postman/cac/Context/Create Context/response.json b/postman/cac/Context/Create Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Create Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Delete Context/event.test.js b/postman/cac/Context/Delete Context/event.test.js new file mode 100644 index 000000000..4482b0ed4 --- /dev/null +++ b/postman/cac/Context/Delete Context/event.test.js @@ -0,0 +1,26 @@ +const host = pm.variables.get("host"); +const context_id = pm.environment.get("context_id"); + +pm.test("204 check", function() { + pm.response.to.have.status(204); +}) + +pm.test("Fetch for context should fail with 404", function () { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + console.log(`alloo ${error}`); + throw error; + } + + pm.expect(response.code).to.be.eq(404); + }); +}) diff --git a/postman/cac/Context/Delete Context/request.json b/postman/cac/Context/Delete Context/request.json new file mode 100644 index 000000000..c550b9968 --- /dev/null +++ b/postman/cac/Context/Delete Context/request.json @@ -0,0 +1,20 @@ +{ + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } +} diff --git a/postman/cac/Context/Delete Context/response.json b/postman/cac/Context/Delete Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Delete Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Get Context/event.test.js b/postman/cac/Context/Get Context/event.test.js new file mode 100644 index 000000000..5ccf43d08 --- /dev/null +++ b/postman/cac/Context/Get Context/event.test.js @@ -0,0 +1,29 @@ +const expected_context = { + "id": pm.environment.get("context_id"), + "value": { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + }, + "override_id": pm.environment.get("override_id"), + "priority": 100, + "override": { + "key1": "value3" + } +}; + +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) + +pm.test("Context equality check", function() { + const response = pm.response.json(); + + delete response.created_at; + delete response.created_by; + + pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context)); +}); diff --git a/postman/cac/Context/Get Context/request.json b/postman/cac/Context/Get Context/request.json new file mode 100644 index 000000000..62678bad8 --- /dev/null +++ b/postman/cac/Context/Get Context/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } +} diff --git a/postman/cac/Context/Get Context/response.json b/postman/cac/Context/Get Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Get Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/List Context/event.test.js b/postman/cac/Context/List Context/event.test.js new file mode 100644 index 000000000..9abf929a8 --- /dev/null +++ b/postman/cac/Context/List Context/event.test.js @@ -0,0 +1,11 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) + + +pm.test("Response validation", function() { + const response = pm.response.json(); + if (response.length == 0) { + throw "list context should return at least one context now" + } +}); diff --git a/postman/cac/Context/List Context/request.json b/postman/cac/Context/List Context/request.json new file mode 100644 index 000000000..ecdd44a16 --- /dev/null +++ b/postman/cac/Context/List Context/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "list" + ] + } +} diff --git a/postman/cac/Context/List Context/response.json b/postman/cac/Context/List Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/List Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Move Context/event.test.js b/postman/cac/Context/Move Context/event.test.js new file mode 100644 index 000000000..4d0bbc826 --- /dev/null +++ b/postman/cac/Context/Move Context/event.test.js @@ -0,0 +1,82 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + if (pm.environment.get("old_context_id") in available_context_ids) { + throw "old context not removed on move" + } + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("old_context_id", pm.environment.get("context_id")); + pm.environment.set("old_override_id", pm.environment.get("override_id")); + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); diff --git a/postman/cac/Context/Move Context/request.json b/postman/cac/Context/Move Context/request.json new file mode 100644 index 000000000..8e6f3ee6f --- /dev/null +++ b/postman/cac/Context/Move Context/request.json @@ -0,0 +1,30 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"tamatar\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } +} diff --git a/postman/cac/Context/Move Context/response.json b/postman/cac/Context/Move Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Move Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Update Context/event.test.js b/postman/cac/Context/Update Context/event.test.js new file mode 100644 index 000000000..ff5ca44b9 --- /dev/null +++ b/postman/cac/Context/Update Context/event.test.js @@ -0,0 +1,77 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); diff --git a/postman/cac/Context/Update Context/request.json b/postman/cac/Context/Update Context/request.json new file mode 100644 index 000000000..547daecfc --- /dev/null +++ b/postman/cac/Context/Update Context/request.json @@ -0,0 +1,28 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } +} diff --git a/postman/cac/Context/Update Context/response.json b/postman/cac/Context/Update Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Update Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Default Config/.meta.json b/postman/cac/Default Config/.meta.json new file mode 100644 index 000000000..1ac6ce34b --- /dev/null +++ b/postman/cac/Default Config/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "Add default-config key" + ] +} diff --git a/postman/cac/Default Config/Add default-config key/event.test.js b/postman/cac/Default Config/Add default-config key/event.test.js new file mode 100644 index 000000000..0a23eb523 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/event.test.js @@ -0,0 +1,33 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(key, value) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const default_configs = resp_obj.default_configs; + + console.log(`Checking if key=${key} with value=${value} in default_configs`); + pm.expect(default_configs[key]).to.be.eq(value); + }); +} + +pm.test("201 check", function () { + pm.response.to.have.status(201); +}) + +pm.test("Check if key added to default config", function () { + const key = "key1", value = "value1"; + getConfigAndTest(key, value); +}); diff --git a/postman/cac/Default Config/Add default-config key/request.json b/postman/cac/Default Config/Add default-config key/request.json new file mode 100644 index 000000000..a289a6aa7 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/request.json @@ -0,0 +1,29 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"value\": \"value1\",\n \"schema\": {\n \"type\": \"string\",\n \"pattern\": \".*\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } +} diff --git a/postman/cac/Default Config/Add default-config key/response.json b/postman/cac/Default Config/Add default-config key/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Dimension/.meta.json b/postman/cac/Dimension/.meta.json new file mode 100644 index 000000000..bc8f8215b --- /dev/null +++ b/postman/cac/Dimension/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "Create Dimension" + ] +} diff --git a/postman/cac/Dimension/Create Dimension/event.test.js b/postman/cac/Dimension/Create Dimension/event.test.js new file mode 100644 index 000000000..dfa01aa5c --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/event.test.js @@ -0,0 +1,3 @@ +pm.test("201 Check", function () { + pm.response.to.have.status(201); +}) \ No newline at end of file diff --git a/postman/cac/Dimension/Create Dimension/request.json b/postman/cac/Dimension/Create Dimension/request.json new file mode 100644 index 000000000..5b3c18f69 --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/request.json @@ -0,0 +1,28 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"dimension\": \"clientId\",\n \"priority\": 100,\n \"type\": \"STRING\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } +} diff --git a/postman/cac/Dimension/Create Dimension/response.json b/postman/cac/Dimension/Create Dimension/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/config/.meta.json b/postman/cac/config/.meta.json new file mode 100644 index 000000000..4afa7da94 --- /dev/null +++ b/postman/cac/config/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "Get Config" + ] +} diff --git a/postman/cac/config/Get Config/event.test.js b/postman/cac/config/Get Config/event.test.js new file mode 100644 index 000000000..f57077057 --- /dev/null +++ b/postman/cac/config/Get Config/event.test.js @@ -0,0 +1,3 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) \ No newline at end of file diff --git a/postman/cac/config/Get Config/request.json b/postman/cac/config/Get Config/request.json new file mode 100644 index 000000000..8eca2d4db --- /dev/null +++ b/postman/cac/config/Get Config/request.json @@ -0,0 +1,19 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" + ], + "path": [ + "config" + ] + } +} diff --git a/postman/cac/config/Get Config/response.json b/postman/cac/config/Get Config/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/config/Get Config/response.json @@ -0,0 +1 @@ +[] From b8d5dceb850e669eefc63a340ab37d2ac6fc04cd Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 27 Jul 2023 18:10:43 +0530 Subject: [PATCH 052/352] refactor: moved AppState & utility fx to new crate --- Cargo.lock | 18 + Cargo.toml | 2 + crates/service-utils/Cargo.lock | 2528 +++++++++++++++++ crates/service-utils/Cargo.toml | 22 + .../service-utils/src}/aws/kms.rs | 7 +- .../service-utils/src}/aws/mod.rs | 0 crates/service-utils/src/db/mod.rs | 1 + {src => crates/service-utils/src}/db/utils.rs | 36 +- crates/service-utils/src/helpers.rs | 47 + crates/service-utils/src/lib.rs | 4 + crates/service-utils/src/service/mod.rs | 1 + .../service-utils/src/service}/types.rs | 12 +- makefile | 2 +- src/api/derived/config.rs | 111 - src/api/derived/context_override.rs | 64 - src/api/derived/mod.rs | 4 - src/api/derived/promote.rs | 68 - src/api/derived/reduce.rs | 75 - src/api/mod.rs | 2 - src/api/primary/context_overrides.rs | 183 -- src/api/primary/dimensions.rs | 112 - src/api/primary/global_config.rs | 120 - src/api/primary/mod.rs | 5 - src/api/primary/new_contexts.rs | 400 --- src/api/primary/overrides.rs | 177 -- src/db/handlers/context_overrides.rs | 73 - src/db/handlers/dimensions.rs | 56 - src/db/handlers/global_config.rs | 55 - src/db/handlers/mod.rs | 5 - src/db/handlers/new_contexts.rs | 87 - src/db/handlers/overrides.rs | 71 - src/db/messages/context_overrides.rs | 26 - src/db/messages/dimensions.rs | 20 - src/db/messages/global_config.rs | 21 - src/db/messages/mod.rs | 5 - src/db/messages/new_contexts.rs | 34 - src/db/messages/overrides.rs | 27 - src/db/mod.rs | 5 - src/db/models/db_models.rs | 60 - .../models/insertables/context_overrides.rs | 11 - src/db/models/insertables/contexts.rs | 11 - src/db/models/insertables/dimensions.rs | 10 - src/db/models/insertables/global_config.rs | 11 - src/db/models/insertables/mod.rs | 6 - src/db/models/insertables/new_contexts.rs | 16 - src/db/models/insertables/overrides.rs | 12 - src/db/models/mod.rs | 2 - src/db/schema.rs | 68 - src/main.rs | 55 +- src/utils/errors.rs | 85 - src/utils/hash.rs | 13 - src/utils/helpers.rs | 28 - src/utils/mod.rs | 4 - src/utils/validations.rs | 141 - src/v1/api/config/handlers.rs | 6 +- src/v1/api/context/handlers.rs | 7 +- src/v1/api/default_config/handlers.rs | 3 +- src/v1/api/dimension/handlers.rs | 6 +- src/v1/api/mod.rs | 1 - src/v1/helpers.rs | 40 - src/v1/mod.rs | 1 - 61 files changed, 2667 insertions(+), 2416 deletions(-) create mode 100644 crates/service-utils/Cargo.lock create mode 100644 crates/service-utils/Cargo.toml rename {src/v1 => crates/service-utils/src}/aws/kms.rs (88%) rename {src/v1 => crates/service-utils/src}/aws/mod.rs (100%) create mode 100644 crates/service-utils/src/db/mod.rs rename {src => crates/service-utils/src}/db/utils.rs (65%) create mode 100644 crates/service-utils/src/helpers.rs create mode 100644 crates/service-utils/src/lib.rs create mode 100644 crates/service-utils/src/service/mod.rs rename {src/v1/api => crates/service-utils/src/service}/types.rs (87%) delete mode 100644 src/api/derived/config.rs delete mode 100644 src/api/derived/context_override.rs delete mode 100644 src/api/derived/mod.rs delete mode 100644 src/api/derived/promote.rs delete mode 100644 src/api/derived/reduce.rs delete mode 100644 src/api/mod.rs delete mode 100644 src/api/primary/context_overrides.rs delete mode 100644 src/api/primary/dimensions.rs delete mode 100644 src/api/primary/global_config.rs delete mode 100644 src/api/primary/mod.rs delete mode 100644 src/api/primary/new_contexts.rs delete mode 100644 src/api/primary/overrides.rs delete mode 100644 src/db/handlers/context_overrides.rs delete mode 100644 src/db/handlers/dimensions.rs delete mode 100644 src/db/handlers/global_config.rs delete mode 100644 src/db/handlers/mod.rs delete mode 100644 src/db/handlers/new_contexts.rs delete mode 100644 src/db/handlers/overrides.rs delete mode 100644 src/db/messages/context_overrides.rs delete mode 100644 src/db/messages/dimensions.rs delete mode 100644 src/db/messages/global_config.rs delete mode 100644 src/db/messages/mod.rs delete mode 100644 src/db/messages/new_contexts.rs delete mode 100644 src/db/messages/overrides.rs delete mode 100644 src/db/mod.rs delete mode 100644 src/db/models/db_models.rs delete mode 100644 src/db/models/insertables/context_overrides.rs delete mode 100644 src/db/models/insertables/contexts.rs delete mode 100644 src/db/models/insertables/dimensions.rs delete mode 100644 src/db/models/insertables/global_config.rs delete mode 100644 src/db/models/insertables/mod.rs delete mode 100644 src/db/models/insertables/new_contexts.rs delete mode 100644 src/db/models/insertables/overrides.rs delete mode 100644 src/db/models/mod.rs delete mode 100644 src/db/schema.rs delete mode 100644 src/utils/errors.rs delete mode 100644 src/utils/hash.rs delete mode 100644 src/utils/helpers.rs delete mode 100644 src/utils/mod.rs delete mode 100644 src/utils/validations.rs diff --git a/Cargo.lock b/Cargo.lock index a14c5c31a..8b42c354a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,6 +620,7 @@ dependencies = [ "rusoto_signature", "serde", "serde_json", + "service-utils", "strum", "strum_macros", "urlencoding", @@ -2199,6 +2200,23 @@ dependencies = [ "serde", ] +[[package]] +name = "service-utils" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "base64 0.21.2", + "bytes", + "diesel", + "dotenv", + "jsonschema", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "urlencoding", +] + [[package]] name = "sha1" version = "0.10.5" diff --git a/Cargo.toml b/Cargo.toml index 0f352efa7..6dd3590d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,5 @@ urlencoding = "~2.1.2" jsonschema = "~0.17" reqwest = { version = "*", features = [ "rustls-tls" ] } json-patch = "1.0.0" + +service-utils = { path = "crates/service-utils" } diff --git a/crates/service-utils/Cargo.lock b/crates/service-utils/Cargo.lock new file mode 100644 index 000000000..14fb97360 --- /dev/null +++ b/crates/service-utils/Cargo.lock @@ -0,0 +1,2528 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags 1.3.2", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.2", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.27", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diesel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" +dependencies = [ + "bitflags 2.3.3", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", + "serde_json", + "uuid", +] + +[[package]] +name = "diesel_derives" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.27", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash 0.8.3", + "anyhow", + "base64 0.21.2", + "bytecount", + "clap", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rusoto_core" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_kms" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "serde", + "serde_json", +] + +[[package]] +name = "rusoto_signature" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" +dependencies = [ + "base64 0.13.1", + "bytes", + "chrono", + "digest 0.9.0", + "futures", + "hex", + "hmac", + "http", + "hyper", + "log", + "md-5", + "percent-encoding", + "pin-project-lite", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "service-utils" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "base64 0.21.2", + "bytes", + "diesel", + "dotenv", + "jsonschema", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "urlencoding", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xml-rs" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml new file mode 100644 index 000000000..6c3efcc77 --- /dev/null +++ b/crates/service-utils/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "service-utils" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = "0.15.0" +# Https server framework +actix = "0.13.0" +actix-web = "4.0.0" +#ORM +diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +rusoto_kms = "0.48.0" +rusoto_signature = "0.48.0" +bytes = "1.4.0" +rusoto_core = "0.48.0" +base64 = "0.21.2" +urlencoding = "~2.1.2" +jsonschema = "~0.17" diff --git a/src/v1/aws/kms.rs b/crates/service-utils/src/aws/kms.rs similarity index 88% rename from src/v1/aws/kms.rs rename to crates/service-utils/src/aws/kms.rs index 8f09ee22e..917ceb15c 100644 --- a/src/v1/aws/kms.rs +++ b/crates/service-utils/src/aws/kms.rs @@ -1,4 +1,4 @@ -use crate::v1::helpers::get_from_env_unsafe; +use crate::helpers::get_from_env_unsafe; use bytes::Bytes; use rusoto_kms::{DecryptRequest, DecryptResponse, Kms, KmsClient}; use rusoto_signature::region::Region; @@ -22,8 +22,9 @@ pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { Ok(DecryptResponse { plaintext: Some(data), .. - }) => String::from_utf8(data.to_vec()) - .expect(format!("Could not convert kms val for {secret_name} to utf8").as_str()), + }) => String::from_utf8(data.to_vec()).expect( + format!("Could not convert kms val for {secret_name} to utf8").as_str(), + ), e => panic!("KMS decryption failed for {secret_name} with error {e:?}"), } } diff --git a/src/v1/aws/mod.rs b/crates/service-utils/src/aws/mod.rs similarity index 100% rename from src/v1/aws/mod.rs rename to crates/service-utils/src/aws/mod.rs diff --git a/crates/service-utils/src/db/mod.rs b/crates/service-utils/src/db/mod.rs new file mode 100644 index 000000000..b5614dd82 --- /dev/null +++ b/crates/service-utils/src/db/mod.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/src/db/utils.rs b/crates/service-utils/src/db/utils.rs similarity index 65% rename from src/db/utils.rs rename to crates/service-utils/src/db/utils.rs index 992f78783..6d7426b0d 100644 --- a/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,25 +1,19 @@ -use actix::{Actor, Addr, SyncContext}; +use crate::aws::kms; +use crate::helpers::get_from_env_unsafe; use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; -use jsonschema::JSONSchema; use urlencoding::encode; -use crate::v1::{aws::kms, helpers::get_from_env_unsafe}; - -//TODOP separate out AppState from DB -pub struct AppState { - pub db: Addr, - pub db_pool: Pool>, - pub default_config_validation_schema: JSONSchema, - pub admin_token: String, -} - -pub struct DbActor(pub Pool>); - -impl Actor for DbActor { - type Context = SyncContext; +async fn get_database_url() -> String { + let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); + let kms_client = kms::new_client(); + let db_password_raw = kms::decrypt(kms_client, "DB_PASSWORD").await; + let db_password = encode(db_password_raw.as_str()).to_string(); + let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); + let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); + format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") } pub async fn get_pool() -> Pool> { @@ -30,13 +24,3 @@ pub async fn get_pool() -> Pool> { .build(manager) .expect("Error building a connection pool") } - -async fn get_database_url() -> String { - let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); - let kms_client = kms::new_client(); - let db_password_raw = kms::decrypt(kms_client, "DB_PASSWORD").await; - let db_password = encode(db_password_raw.as_str()).to_string(); - let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); - let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); - format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") -} diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs new file mode 100644 index 000000000..f1778f376 --- /dev/null +++ b/crates/service-utils/src/helpers.rs @@ -0,0 +1,47 @@ +use actix_web::{error::ErrorInternalServerError, Error}; +use std::{env::VarError, fmt, str::FromStr}; + +//WARN Do NOT use this fxn inside api requests, instead add the required +//env to AppState and get value from there. As this panics, it should +//only be used for envs needed during app start. +pub fn get_from_env_unsafe(name: &str) -> Result +where + F: FromStr, + ::Err: std::fmt::Debug, +{ + std::env::var(name) + .map(|val| val.parse().unwrap()) + .map_err(|e| { + println!("{name} env not found with error: {e}"); + return e; + }) +} + +pub trait ToActixErr { + fn map_err_to_internal_server( + self, + log_prefix: &str, + err_body: B, + ) -> Result + where + B: fmt::Debug + fmt::Display + 'static; +} + +impl ToActixErr for Result +where + E: fmt::Debug, +{ + fn map_err_to_internal_server( + self, + log_prefix: &str, + err_body: B, + ) -> Result + where + B: fmt::Debug + fmt::Display + 'static, + { + self.map_err(|e| { + println!("{log_prefix}, err: {e:?}"); + ErrorInternalServerError(err_body) + }) + } +} diff --git a/crates/service-utils/src/lib.rs b/crates/service-utils/src/lib.rs new file mode 100644 index 000000000..e846a2af8 --- /dev/null +++ b/crates/service-utils/src/lib.rs @@ -0,0 +1,4 @@ +pub mod aws; +pub mod db; +pub mod helpers; +pub mod service; diff --git a/crates/service-utils/src/service/mod.rs b/crates/service-utils/src/service/mod.rs new file mode 100644 index 000000000..cd408564e --- /dev/null +++ b/crates/service-utils/src/service/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/src/v1/api/types.rs b/crates/service-utils/src/service/types.rs similarity index 87% rename from src/v1/api/types.rs rename to crates/service-utils/src/service/types.rs index 0c9c69c67..2b5a4edda 100644 --- a/src/v1/api/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -1,3 +1,9 @@ +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; +use jsonschema::JSONSchema; + use std::{ future::{ready, Ready}, println, @@ -5,7 +11,11 @@ use std::{ use actix_web::{error, web::Data, Error, FromRequest}; -use crate::db::utils::AppState; +pub struct AppState { + pub db_pool: Pool>, + pub default_config_validation_schema: JSONSchema, + pub admin_token: String, +} #[derive(Clone)] pub struct AuthenticationInfo(pub String); diff --git a/makefile b/makefile index 550900255..5cbfcfa68 100644 --- a/makefile +++ b/makefile @@ -34,7 +34,7 @@ run: #populate the kms keyId sleep 10 #TODO move this sleep to aws cli list-keys command instead cargo build --color always -# diesel migration run + diesel migration run source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always diff --git a/src/api/derived/config.rs b/src/api/derived/config.rs deleted file mode 100644 index 9691fce16..000000000 --- a/src/api/derived/config.rs +++ /dev/null @@ -1,111 +0,0 @@ -// TODO :: Handle errors with appropriate error message - -use std::collections::HashMap; - -use actix_web::{ - get, - web::{Data, Json}, - Either::Left, - HttpRequest, -}; -use serde_json::{to_value, Error, Value}; - -use crate::api::primary::{ - context_overrides::fetch_override_from_ctx_id, global_config::get_complete_config, - new_contexts::fetch_new_contexts, overrides::get_override_helper, -}; - -use crate::utils::errors::{ - AppError, - AppErrorType::{DBError, SomethingWentWrong}, -}; -use crate::utils::helpers::strip_double_quotes; - -use crate::AppState; - -fn default_parsing_error(err: Error) -> AppError { - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - } -} - -async fn get_new_context_overrides_object( - state: &Data, - query_string: &str, -) -> Result { - let raw_contexts = fetch_new_contexts(state, query_string.to_string()).await?; - - let mut contexts = Vec::new(); - let mut override_map = HashMap::new(); - - for item in raw_contexts { - match (item.get("id"), item.get("condition")) { - (Some(context_id), Some(condition)) => { - if let Ok(override_id) = - fetch_override_from_ctx_id(&state, strip_double_quotes(&context_id.to_string())) - .await - { - let fetched_override_value = - get_override_helper(&state, override_id.to_owned()).await?; - - override_map.insert(override_id.to_owned(), fetched_override_value); - - contexts.push( - to_value(HashMap::from([ - ( - "overrideWithKeys", - &to_value(override_id).map_err(default_parsing_error)?, - ), - ("condition", condition), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?, - ); - } - } - (_, _) => (), - } - } - - to_value(HashMap::from([ - ( - "context", - to_value(contexts).map_err(default_parsing_error)?, - ), - ( - "overrides", - to_value(override_map).map_err(default_parsing_error)?, - ), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }) -} - -#[get("")] -pub async fn get_config(state: Data, req: HttpRequest) -> Result, AppError> { - let query_string = req.query_string(); - - Ok(Json( - to_value(HashMap::from([ - ("global_config", get_complete_config(&state).await?), - // ("context_overrides", get_context_overrides_object(&state, query_string).await?) - ( - "context_overrides", - get_new_context_overrides_object(&state, query_string).await?, - ), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?, - )) -} diff --git a/src/api/derived/context_override.rs b/src/api/derived/context_override.rs deleted file mode 100644 index 77e04e868..000000000 --- a/src/api/derived/context_override.rs +++ /dev/null @@ -1,64 +0,0 @@ -// TODO :: Handle errors with appropriate error message -use std::collections::HashMap; - -use actix_web::{ - post, - web::{Data, Json}, - Either::Left, -}; - -use log::info; -use serde::{Deserialize, Serialize}; -use serde_json::{to_value, Value}; - -use crate::utils::errors::{AppError, AppErrorType::SomethingWentWrong}; - -use crate::api::primary::{ - // contexts::add_new_context, - context_overrides::add_ctx_override, - new_contexts::add_new_context_v2, - overrides::add_new_override, -}; - -use crate::AppState; - -#[derive(Deserialize, Serialize)] -pub struct KeyValue { - context_value: Value, - override_value: Value, -} - -#[post("")] -pub async fn add_new_context_override( - state: Data, - body: Json, -) -> Result, AppError> { - let override_value_from_body = body.override_value.clone(); - let context_value_from_body = body.context_value.clone(); - - info!("Prithiv {:?}", context_value_from_body); - - // ! Create transaction - // ! TODO:: Fix this asap - - // Ignore even there is an existing context - // let context_id = add_new_context(&state, context_value_from_body).await?; - let context_id = add_new_context_v2(&state, context_value_from_body, true).await?; - - // Ignore even there is an existing override - let override_id = add_new_override(&state, override_value_from_body.to_owned(), true).await?; - - add_ctx_override(&state, context_id.clone().id, override_id.clone().id, true).await?; - - Ok(Json( - to_value(HashMap::from([ - ("context_id", context_id.id), - ("override_id", override_id.id), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - })?, - )) -} diff --git a/src/api/derived/mod.rs b/src/api/derived/mod.rs deleted file mode 100644 index 0b856ed49..000000000 --- a/src/api/derived/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod config; -pub mod context_override; -pub mod promote; -pub mod reduce; diff --git a/src/api/derived/promote.rs b/src/api/derived/promote.rs deleted file mode 100644 index 59fbfeb8d..000000000 --- a/src/api/derived/promote.rs +++ /dev/null @@ -1,68 +0,0 @@ -// TODO :: Handle errors with appropriate error message -use actix_web::{ - post, - web::{Data, Json}, - Either::Left, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{to_value, Error, Value}; - -use crate::api::primary::{ - context_overrides::{ - add_ctx_override, delete_ctx_override_helper, fetch_override_from_ctx_id, - ContextOverrideResponse, - }, - new_contexts::{add_new_context_v2, delete_new_context_helper, process_input_context_json}, -}; -use crate::utils::{ - errors::{AppError, AppErrorType::SomethingWentWrong}, - hash::string_based_b64_hash, -}; -use crate::AppState; - -fn default_parsing_error(err: Error) -> AppError { - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct KeyValue { - context_value: Value, - dimension_to_be_dropped: String, -} - -#[post("")] -pub async fn promote_contexts_overrides( - input_state: Data, - body: Json, -) -> Result, AppError> { - let state = &input_state; - - let input_context = body.context_value.clone(); - let dimension_to_be_dropped = body.dimension_to_be_dropped.clone(); - - let get_processed_context_and_hash_values = - |keys_to_be_excluded: Option<&Vec>| -> Result<(String, Value), AppError> { - let (processed_context_value, _) = - process_input_context_json(&input_context, keys_to_be_excluded)?; - let context_value = to_value(processed_context_value).map_err(default_parsing_error)?; - let hashed_value = string_based_b64_hash(context_value.to_string()).to_string(); - - Ok((hashed_value, context_value)) - }; - - let (existing_hashed_value, _) = get_processed_context_and_hash_values(None)?; - let (_, new_context_value) = - get_processed_context_and_hash_values(Some(&Vec::from([dimension_to_be_dropped])))?; - - // Add transaction wrapper here - let override_id = fetch_override_from_ctx_id(&state, &existing_hashed_value).await?; - let context_response = add_new_context_v2(&state, new_context_value, true).await?; - - delete_new_context_helper(&state, existing_hashed_value.clone()).await?; - delete_ctx_override_helper(&state, existing_hashed_value).await?; - add_ctx_override(&state, context_response.id, override_id, true).await -} diff --git a/src/api/derived/reduce.rs b/src/api/derived/reduce.rs deleted file mode 100644 index 8a94da376..000000000 --- a/src/api/derived/reduce.rs +++ /dev/null @@ -1,75 +0,0 @@ -// TODO :: Handle errors with appropriate error message -use std::collections::HashSet; - -use actix_web::{ - delete, - web::{Data, Json}, -}; - -use crate::utils::errors::AppError; - -use crate::api::primary::{ - context_overrides::fetch_all_ctx_overrides, - new_contexts::{delete_new_context_helper, fetch_all_new_contexts}, - overrides::{delete_override_helper, get_all_overrides}, -}; - -use crate::AppState; - -use std::iter::FromIterator; - -fn find_diff(all_keys: &Vec, sub_set_keys: &Vec) -> Vec { - let a_set: HashSet = HashSet::from_iter(sub_set_keys.iter().cloned()); - let mut difference = Vec::new(); - - for i in all_keys { - if !a_set.contains(i) { - difference.push(i.to_owned()); - } - } - - difference -} - -#[delete("")] -pub async fn reduce_contexts_overrides( - input_state: Data, -) -> Result, AppError> { - let state = &input_state; - - let ctx_overrides = fetch_all_ctx_overrides(state).await?; - - let mapped_context_ids: Vec = ctx_overrides - .iter() - .map(|i| i.context_id.to_owned()) - .collect(); - - let mapped_override_ids: Vec = ctx_overrides - .iter() - .map(|i| i.override_id.to_owned()) - .collect(); - - let all_context_ids: Vec = fetch_all_new_contexts(state) - .await? - .iter() - .map(|x| x.key.to_owned()) - .collect(); - let all_override_ids: Vec = get_all_overrides(state) - .await? - .iter() - .map(|x| x.key.to_owned()) - .collect(); - - let extra_context_ids = find_diff(&all_context_ids, &mapped_context_ids); - let extra_override_ids = find_diff(&all_override_ids, &mapped_override_ids); - - for item in extra_context_ids { - delete_new_context_helper(&state, item).await?; - } - - for item in extra_override_ids { - delete_override_helper(&state, item).await?; - } - - Ok(Json("Okay".to_string())) -} diff --git a/src/api/mod.rs b/src/api/mod.rs deleted file mode 100644 index 5e152c1b3..000000000 --- a/src/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod derived; -pub mod primary; diff --git a/src/api/primary/context_overrides.rs b/src/api/primary/context_overrides.rs deleted file mode 100644 index c33b97744..000000000 --- a/src/api/primary/context_overrides.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::collections::HashMap; - -use actix::Addr; -use actix_web::{ - delete, get, post, - web::{Data, Json, Path}, - Either::Left, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{to_value, Value}; - -use crate::{ - db::messages::context_overrides::{ - CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides, - }, - db::models::db_models::CtxOverrides, - AppState, DbActor, -}; - -use crate::utils::errors::{ - AppError, - AppErrorType::{DBError, DataExists, NotFound}, -}; - -#[derive(Deserialize)] -pub struct BodyType { - context_id: String, - override_id: String, -} - -#[derive(Serialize)] -pub struct ContextOverrideResponse { - context_id: String, -} - -// TODO :: Have to re-think and re-implement all these apis -pub async fn add_ctx_override( - state: &Data, - context_id: String, - override_id: String, - return_if_present: bool, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db - .send(CreateCtxOverrides { - context_id: context_id.to_owned(), - override_id, - }) - .await - { - Ok(Ok(result)) => Ok(Json(ContextOverrideResponse { - context_id: result.context_id, - })), - Ok(Err(err)) => { - if return_if_present { - Ok(Json(ContextOverrideResponse { context_id })) - } else { - Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }) - } - } - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn fetch_override_from_ctx_id( - state: &Data, - context_id: &str, -) -> Result { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(FetchCtxOverrides { - context_id: context_id.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(result.override_id), - Ok(Err(err)) => Err(AppError { - message: Some("failed to fetch key value".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn fetch_all_ctx_overrides( - state: &Data, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db.send(FetchAllCtxOverrides).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(AppError { - message: Some("failed to fetch key value".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[post("")] -pub async fn post_ctx_override( - state: Data, - body: Json, -) -> Result, AppError> { - let ctx_id: String = body.context_id.clone(); - let ovr_id: String = body.override_id.clone(); - add_ctx_override(&state, ctx_id, ovr_id, false).await -} - -#[get("/{id}")] -pub async fn get_ctx_override( - state: Data, - id: Path, -) -> Result, AppError> { - let context_id = id.to_string(); - let override_id = fetch_override_from_ctx_id(&state, &context_id).await?; - - Ok(Json( - to_value(HashMap::from([ - ("context_id", context_id), - ("override_id", override_id), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?, - )) -} - -pub async fn delete_ctx_override_helper( - state: &Data, - id: String, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db - .send(DeleteCtxOverrides { - context_id: id.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(serde_json::Value::String(result.context_id))), - Ok(Err(err)) => Err(AppError { - message: Some("Data not found for context override deletion".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[delete("/{id}")] -pub async fn delete_ctx_override( - state: Data, - id: Path, -) -> Result, AppError> { - delete_ctx_override_helper(&state, id.to_string()).await -} diff --git a/src/api/primary/dimensions.rs b/src/api/primary/dimensions.rs deleted file mode 100644 index 514696083..000000000 --- a/src/api/primary/dimensions.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::db::models::db_models::Dimension; -use actix_web::{ - get, post, - web::{Data, Json, Path}, - Either::Left, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - db::messages::dimensions::{CreateDimension, FetchDimension, FetchDimensions}, - AppState, DbActor, -}; - -use actix::Addr; - -use crate::utils::errors::{ - AppError, - AppErrorType::{DBError, DataExists, NotFound}, -}; - -// Get dimension table -#[get("")] -pub async fn get_dimensions(state: Data) -> Result>, AppError> { - fetch_dimensions(&state).await.map(Json) -} - -pub async fn fetch_dimensions(state: &Data) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db.send(FetchDimensions).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(AppError { - message: Some("failed to get dimensions".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -// Get request to fetch dimension from dimension name -#[derive(Deserialize, Serialize)] -pub struct Key { - dimension: String, -} - -#[get("/{dimension}")] -pub async fn get_dimension_key( - state: Data, - params: Path, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - let dimension_key = params.into_inner().dimension; - - match db - .send(FetchDimension { - dimension: dimension_key, - }) - .await - { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to get required dimension".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -// Post request to add key, value to dimension table -#[derive(Deserialize, Serialize, Clone)] -pub struct KeyValue { - dimension: String, - priority: i32, -} - -#[post("")] -pub async fn post_dimension( - state: Data, - body: Json, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(CreateDimension { - dimension: body.dimension.clone(), - priority: body.priority, - }) - .await - { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to add dimension".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} diff --git a/src/api/primary/global_config.rs b/src/api/primary/global_config.rs deleted file mode 100644 index 00753748a..000000000 --- a/src/api/primary/global_config.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; - -use crate::db::models::db_models::GlobalConfig; -use crate::utils::errors::{ - AppError, - AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, -}; -use crate::{ - db::messages::global_config::{CreateGlobalKey, FetchConfigKey, FetchGlobalConfig}, - AppState, DbActor, -}; -use actix::Addr; -use actix_web::{ - get, post, - web::{Data, Json, Path}, - Either::Left, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{from_value, to_value, Value}; - -async fn get_all_rows_from_global_config( - state: &Data, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - match db.send(FetchGlobalConfig).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(AppError { - message: Some("config not found".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn get_complete_config(state: &Data) -> Result { - let db_rows = get_all_rows_from_global_config(&state).await?; - let mut hash_map: HashMap = HashMap::new(); - - for row in db_rows { - hash_map.insert(row.key, row.value); - } - - to_value(&hash_map).map_err(|err| AppError { - message: Some("Unable to fetch global config".to_string()), - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - }) -} - -// Get whole global config -#[get("")] -pub async fn get_global_config(state: Data) -> Result, AppError> { - Ok(Json(get_complete_config(&state).await?)) -} - -// Get request to fetch value for given key -#[derive(Deserialize, Serialize)] -pub struct Key { - key: String, -} - -#[get("/{key}")] -pub async fn get_global_config_key( - state: Data, - params: Path, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - let key = params.into_inner().key; - - match db.send(FetchConfigKey { key }).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to fetch global config".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[post("")] -pub async fn post_config_key_value( - state: Data, - body: Json, -) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - let b_tree: BTreeMap = from_value(body.clone()).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?; - - for (key, value) in b_tree { - match db.send(CreateGlobalKey { key, value }).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to add new key to global config".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - }?; - } - - Ok(body) -} diff --git a/src/api/primary/mod.rs b/src/api/primary/mod.rs deleted file mode 100644 index 5701bbda5..000000000 --- a/src/api/primary/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod context_overrides; -pub mod dimensions; -pub mod global_config; -pub mod new_contexts; -pub mod overrides; diff --git a/src/api/primary/new_contexts.rs b/src/api/primary/new_contexts.rs deleted file mode 100644 index 70fdffe6b..000000000 --- a/src/api/primary/new_contexts.rs +++ /dev/null @@ -1,400 +0,0 @@ -use std::{cmp::Ordering, collections::HashMap}; - -use actix::Addr; -use actix_web::{ - delete, get, post, - web::{Data, Json, Path}, - Either::Left, - HttpRequest, -}; -use serde::Serialize; -use serde_json::{from_value, to_value, Error, Value}; - -use crate::{ - api::primary::dimensions::fetch_dimensions, - db::messages::new_contexts::{ - CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext, - }, - AppState, DbActor, -}; - -use crate::db::models::db_models::NewContexts; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{DBError, DataExists, NotFound, SomethingWentWrong}, - }, - hash::string_based_b64_hash, - helpers::{split_stringified_key_value_pair, strip_double_quotes}, -}; - -#[derive(Serialize, Clone)] -pub struct ContextIdResponse { - pub id: String, -} - -fn default_parsing_error(err: Error) -> AppError { - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong, - } -} - -fn get_dimension_name(idx: usize) -> String { - let dimesions = Vec::from(["tier", "merchantId", "os", "country"]); - dimesions[idx].to_string() -} - -type ContextValueAndDimensionMap = (HashMap, HashMap); -type MaybeContextValueAndDimensionMap = (Option>, HashMap); - -fn contexts_comparator( - priority_map: HashMap, -) -> impl Fn(&NewContexts, &NewContexts) -> Ordering { - let get_value_from_map = move |key: Option| { - if let Some(val) = key { - if let Some(val1) = priority_map.get(&val) { - return val1.to_owned(); - } - } - - return 0; - }; - - move |ctx1: &NewContexts, ctx2: &NewContexts| { - let val1 = get_value_from_map(ctx1.column1.to_owned().map(|_| get_dimension_name(0))) - + get_value_from_map(ctx1.column2.to_owned().map(|_| get_dimension_name(1))) - + get_value_from_map(ctx1.column3.to_owned().map(|_| get_dimension_name(2))) - + get_value_from_map(ctx1.column4.to_owned().map(|_| get_dimension_name(3))); - - let val2 = get_value_from_map(ctx2.column1.to_owned().map(|_| get_dimension_name(0))) - + get_value_from_map(ctx2.column2.to_owned().map(|_| get_dimension_name(1))) - + get_value_from_map(ctx2.column3.to_owned().map(|_| get_dimension_name(2))) - + get_value_from_map(ctx2.column4.to_owned().map(|_| get_dimension_name(3))); - - val1.cmp(&val2) - } -} - -pub async fn fetch_raw_context_v2( - state: &Data, - column1: Option<&String>, - column2: Option<&String>, - column3: Option<&String>, - column4: Option<&String>, - dimesion_map: HashMap, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - - let result: Vec = match db - .send(FetchNewContext { - column1: column1.map(|x| x.to_string()), - column2: column2.map(|x| x.to_string()), - column3: column3.map(|x| x.to_string()), - column4: column4.map(|x| x.to_string()), - }) - .await - { - Ok(Ok(res)) => Ok(res), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - }?; - - let mut final_result: Vec = result - .into_iter() - .filter(|y| { - let x = y.to_owned(); - let column1_from_row: Option = x.column1.to_owned(); - let column2_from_row: Option = x.column2.to_owned(); - let column3_from_row: Option = x.column3.to_owned(); - let column4_from_row: Option = x.column4.to_owned(); - - (column1_from_row.is_none() - || column1.is_none() - || column1_from_row == column1.map(|x| x.to_string())) - && (column2_from_row.is_none() - || column2.is_none() - || column2_from_row == column2.map(|x| x.to_string())) - && (column3_from_row.is_none() - || column3.is_none() - || column3_from_row == column3.map(|x| x.to_string())) - && (column4_from_row.is_none() - || column4.is_none() - || column4_from_row == column4.map(|x| x.to_string())) - }) - .collect(); - - final_result.sort_by(contexts_comparator(dimesion_map)); - - Ok(final_result) -} - -pub fn process_single_condition( - value_object: &HashMap, - keys_to_be_excluded: Option<&Vec>, -) -> MaybeContextValueAndDimensionMap { - let mut result_map: HashMap = HashMap::new(); - let mut result_context: Option> = None; - - // Range check have to be implemented - if value_object.contains_key("==") { - let variable_array_value = value_object.get("==").unwrap(); - let variable_array: Vec = from_value(variable_array_value.to_owned()).unwrap(); - - if variable_array.len() >= 2 { - let variable_map: HashMap = - from_value(variable_array[0].to_owned()).unwrap(); - let mapped_value = variable_array[1].to_string(); - - if let Some(key) = variable_map.get("var") { - let key_string = strip_double_quotes(&key.to_string()).to_string(); - let to_be_included = - keys_to_be_excluded.map_or(true, |vect| !vect.contains(&key_string)); - - if to_be_included { - result_map.insert( - // Removing double quotes at the start and end for both key and value - strip_double_quotes(&key_string).to_owned(), - strip_double_quotes(&mapped_value).to_owned(), - ); - - result_context = Some(value_object.to_owned()); - } - } - } - } - - (result_context, result_map) -} - -pub fn process_input_context_json( - input_json: &Value, - keys_to_be_excluded: Option<&Vec>, -) -> Result { - let input_as_map: HashMap = - from_value(input_json.to_owned()).map_err(default_parsing_error)?; - let mut result_context_array = Vec::new(); - - // Single dimension or condition - if !input_as_map.contains_key("and") { - // ! TODO :: Fix this asap - return Err(AppError { - message: None, - cause: Some(Left("Case yet to handled properly".to_string())), - status: SomethingWentWrong, - }); - // return Ok(process_single_condition(&input_as_map, keys_to_be_excluded)); - } - - let mut result_map = HashMap::new(); - let val = input_as_map.get("and").unwrap(); - let multiple_condition_array: Vec = from_value(val.to_owned()).unwrap(); - - for item in multiple_condition_array { - let value_object: HashMap = - from_value(item).map_err(default_parsing_error)?; - let (maybe_ctx, map) = process_single_condition(&value_object, keys_to_be_excluded); - - if let Some(ctx) = maybe_ctx { - result_context_array.push(ctx); - result_map.extend(map); - } - } - - Ok(( - HashMap::from([( - "and".to_string(), - to_value(result_context_array).map_err(default_parsing_error)?, - )]), - result_map, - )) -} - -pub async fn add_new_context_v2( - state: &Data, - raw_context_value: Value, - return_if_present: bool, -) -> Result { - let db: Addr = state.db.clone(); - - let (processed_context_value, key_value_map) = - process_input_context_json(&raw_context_value, None)?; - let context_value = to_value(processed_context_value).map_err(default_parsing_error)?; - - // ? TODO :: Post as an array of value - // ? TODO :: Sort query based on key and add to DB - // let ss = Json(&context_value); - // let hashed_value = "test12".to_string(); - - let hashed_value = string_based_b64_hash(context_value.to_string()).to_string(); - - match db - .send(CreateNewContext { - key: hashed_value.to_owned(), - value: context_value, - column1: key_value_map - .get(&get_dimension_name(0)) - .map(|x| x.to_string()), - column2: key_value_map - .get(&get_dimension_name(1)) - .map(|x| x.to_string()), - column3: key_value_map - .get(&get_dimension_name(2)) - .map(|x| x.to_string()), - column4: key_value_map - .get(&get_dimension_name(3)) - .map(|x| x.to_string()), - }) - .await - { - Ok(Ok(result)) => Ok(ContextIdResponse { id: result.key }), - Ok(Err(err)) => { - if return_if_present { - Ok(ContextIdResponse { id: hashed_value }) - } else { - Err(AppError { - message: Some("Failed to add context".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }) - } - } - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn fetch_all_new_contexts(state: &Data) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db.send(FetchAllNewContexts).await { - Ok(Ok(res)) => Ok(res), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn delete_new_context_helper( - state: &Data, - key: String, -) -> Result { - let db: Addr = state.db.clone(); - - match db.send(DeleteNewContext { key }).await { - Ok(Ok(res)) => Ok(res), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to delete context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn fetch_new_contexts( - state: &Data, - query_string: String, -) -> Result>, AppError> { - let key_value_pairs = split_stringified_key_value_pair(&query_string); - - let all_dimesions = fetch_dimensions(&state).await?; - - let mut dimesion_map = HashMap::new(); - - for item in all_dimesions { - dimesion_map.insert(item.dimension, item.priority); - } - - let mut filter_input_keys_map = HashMap::new(); - - for item in key_value_pairs { - if dimesion_map.contains_key(item[0]) { - filter_input_keys_map.insert(item[0].to_string(), item[1].to_string()); - } - } - - let raw_contexts = fetch_raw_context_v2( - &state, - filter_input_keys_map.get(&get_dimension_name(0)), - filter_input_keys_map.get(&get_dimension_name(1)), - filter_input_keys_map.get(&get_dimension_name(2)), - filter_input_keys_map.get(&get_dimension_name(3)), - dimesion_map, - ) - .await?; - - let mut formatted_contexts = Vec::new(); - for item in raw_contexts { - formatted_contexts.push(HashMap::from([ - ("condition", item.value), - ("id", to_value(item.key).map_err(default_parsing_error)?), - ])); - } - - Ok(formatted_contexts) -} - -#[get("")] -pub async fn get_new_context( - state: Data, - req: HttpRequest, -) -> Result, AppError> { - let query_string = req.query_string(); - let formatted_contexts = fetch_new_contexts(&state, query_string.to_string()).await?; - let mut result = Vec::new(); - - for item in &formatted_contexts { - result.push(HashMap::from([( - "condition", - item.get("condition").to_owned(), - )])); - } - - Ok(Json(to_value(result).map_err(default_parsing_error)?)) -} - -#[post("")] -pub async fn post_new_context( - state: Data, - body: Json, -) -> Result, AppError> { - let context_value = body.clone(); - Ok(Json( - add_new_context_v2(&state, context_value, false).await?, - )) -} - -#[delete("/{id}")] -pub async fn delete_new_context( - state: Data, - id: Path, -) -> Result, AppError> { - Ok(Json( - delete_new_context_helper(&state, id.to_string()).await?, - )) -} diff --git a/src/api/primary/overrides.rs b/src/api/primary/overrides.rs deleted file mode 100644 index 2cbcf568c..000000000 --- a/src/api/primary/overrides.rs +++ /dev/null @@ -1,177 +0,0 @@ -use actix::Addr; -use actix_web::{ - delete, get, post, - web::{Data, Json, Path}, - Either::{Left, Right}, -}; -use serde::Serialize; -use serde_json::Value; - -use crate::{ - db::messages::overrides::{CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride}, - db::models::db_models::Overrides, - AppState, DbActor, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{BadRequest, DBError, DataExists, NotFound, SomethingWentWrong}, - }, - hash::string_based_b64_hash, - helpers::sort_multi_level_keys_in_stringified_json, - validations::validate_sub_tree, -}; - -use crate::api::primary::global_config::get_complete_config; - -#[derive(Serialize, Clone)] -pub struct OverrideIdResponse { - pub id: String, -} - -pub async fn add_new_override( - state: &Data, - override_value: Value, - return_if_present: bool, -) -> Result { - let db: Addr = state.db.clone(); - - let global_config_as_value = get_complete_config(&state).await?; - - if let Err(error_message) = validate_sub_tree(&global_config_as_value, &override_value) { - return Err(AppError { - message: Some("Validation failed".to_string()), - cause: Some(Right(error_message)), - status: BadRequest, - }); - } - - // TODO :: Post as an array of value - let formatted_value = sort_multi_level_keys_in_stringified_json(override_value) - // TODO :: Fix this properly - // .ok_or(OverrideError::ErrorOnParsingBody {error_message : to_value("Error on sorting keys".to_string())})?; - .ok_or(AppError { - message: Some("Unable to parse override value".to_string()), - cause: None, - status: SomethingWentWrong, - })?; - - let hashed_value = string_based_b64_hash((&formatted_value).to_string()).to_string(); - - match db - .send(CreateOverride { - key: hashed_value.to_owned(), - value: formatted_value, - }) - .await - { - Ok(Ok(result)) => Ok(OverrideIdResponse { id: result.key }), - Ok(Err(err)) => { - if return_if_present { - Ok(OverrideIdResponse { id: hashed_value }) - } else { - Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists, - }) - } - } - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[post("")] -pub async fn post_override( - state: Data, - body: Json, -) -> Result, AppError> { - let override_value = body.clone(); - Ok(Json(add_new_override(&state, override_value, false).await?)) -} - -pub async fn get_override_helper( - state: &Data, - key: String, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db.send(FetchOverride { key }).await { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to fetch value for given override key".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -pub async fn get_all_overrides(state: &Data) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db.send(FetchAllOverrides).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to fetch value for given override key".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[get("/{key}")] -pub async fn get_override( - state: Data, - key: Path, -) -> Result, AppError> { - get_override_helper(&state, key.to_owned()).await -} - -pub async fn delete_override_helper( - state: &Data, - id: String, -) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db - .send(DeleteOverride { - key: id.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("Data not found for override deletion".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound, - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }), - } -} - -#[delete("/{key}")] -pub async fn delete_override( - state: Data, - id: Path, -) -> Result, AppError> { - delete_override_helper(&state, id.to_string()).await -} diff --git a/src/db/handlers/context_overrides.rs b/src/db/handlers/context_overrides.rs deleted file mode 100644 index d6b22b57b..000000000 --- a/src/db/handlers/context_overrides.rs +++ /dev/null @@ -1,73 +0,0 @@ -use diesel::QueryResult; - -use crate::db::models::{ - db_models::CtxOverrides, insertables::context_overrides::CtxOverrideInsertion, -}; -use crate::db::utils::DbActor; - -use crate::db::messages::context_overrides::{ - CreateCtxOverrides, DeleteCtxOverrides, FetchAllCtxOverrides, FetchCtxOverrides, -}; -use crate::db::schema::ctxoverrides::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for creating context override"); - - diesel::insert_into(ctxoverrides) - .values(CtxOverrideInsertion { - context_id: msg.context_id, - override_id: msg.override_id, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - - ctxoverrides - .filter(context_id.eq(msg.context_id)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchAllCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - ctxoverrides.get_results::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - - diesel::delete(ctxoverrides) - .filter(context_id.eq(msg.context_id)) - .get_result::(&mut conn) - } -} diff --git a/src/db/handlers/dimensions.rs b/src/db/handlers/dimensions.rs deleted file mode 100644 index 11ec20997..000000000 --- a/src/db/handlers/dimensions.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::db::models::db_models::Dimension; -use crate::db::utils::DbActor; - -use crate::db::messages::dimensions::{CreateDimension, FetchDimension, FetchDimensions}; -use crate::db::models::insertables::dimensions::NewDimension; -use crate::db::schema::dimensions::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchDimensions, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Fetch Dimensions: Unable to establish connection"); - - dimensions.get_results::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Fetch Dimension: Unable to establish connection"); - - dimensions - .filter(dimension.eq(msg.dimension)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Create Dimension: Unable to establish connection"); - - let new_dimension = NewDimension { - dimension: msg.dimension, - priority: msg.priority, - }; - - diesel::insert_into(dimensions) - .values(new_dimension) - .get_result::(&mut conn) - } -} diff --git a/src/db/handlers/global_config.rs b/src/db/handlers/global_config.rs deleted file mode 100644 index b086efdbc..000000000 --- a/src/db/handlers/global_config.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::db::models::{db_models::GlobalConfig, insertables::global_config::NewGlobalConfigKey}; -use crate::db::utils::DbActor; - -use crate::db::messages::global_config::{CreateGlobalKey, FetchConfigKey, FetchGlobalConfig}; -use crate::db::schema::global_config::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchGlobalConfig, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Fetch GlobalConfig: Unable to establish connection"); - - global_config.get_results::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchConfigKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Fetch Dimension: Unable to establish connection"); - - global_config - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateGlobalKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Create Dimension: Unable to establish connection"); - - let new_key = NewGlobalConfigKey { - key: msg.key, - value: msg.value, - }; - - diesel::insert_into(global_config) - .values(new_key) - .get_result::(&mut conn) - } -} diff --git a/src/db/handlers/mod.rs b/src/db/handlers/mod.rs deleted file mode 100644 index 5701bbda5..000000000 --- a/src/db/handlers/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod context_overrides; -pub mod dimensions; -pub mod global_config; -pub mod new_contexts; -pub mod overrides; diff --git a/src/db/handlers/new_contexts.rs b/src/db/handlers/new_contexts.rs deleted file mode 100644 index 08005e8c8..000000000 --- a/src/db/handlers/new_contexts.rs +++ /dev/null @@ -1,87 +0,0 @@ -use diesel::QueryResult; - -use crate::db::models::{db_models::NewContexts, insertables::new_contexts::NewContextInsertion}; -use crate::db::utils::DbActor; - -use crate::db::messages::new_contexts::{ - CreateNewContext, DeleteNewContext, FetchAllNewContexts, FetchNewContext, -}; -use crate::db::schema::newcontexts::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for creating context override"); - - diesel::insert_into(newcontexts) - .values(NewContextInsertion { - key: msg.key, - value: msg.value, - column1: msg.column1, - column2: msg.column2, - column3: msg.column3, - column4: msg.column4, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - - // ! TODO :: Move filter directly over here (DB scanning) - newcontexts.get_results::(&mut conn) - // .filter( - - // (column1 - // .eq(msg.column1) - // .or(column1.is_null()) - // ) - - // .and( - // column2 - // .eq(msg.column2) - // .or(column2.is_null()) - // ) - // ) - } -} - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchAllNewContexts, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - newcontexts.get_results::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteNewContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching context override"); - - diesel::delete(newcontexts) - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} diff --git a/src/db/handlers/overrides.rs b/src/db/handlers/overrides.rs deleted file mode 100644 index 28b5a3e27..000000000 --- a/src/db/handlers/overrides.rs +++ /dev/null @@ -1,71 +0,0 @@ -use diesel::QueryResult; - -use crate::db::models::{db_models::Overrides, insertables::overrides::NewOverride}; -use crate::db::utils::DbActor; - -use crate::db::messages::overrides::{ - CreateOverride, DeleteOverride, FetchAllOverrides, FetchOverride, -}; -use crate::db::schema::overrides::dsl::*; -use actix::Handler; -use diesel::{self, prelude::*}; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for creating override"); - - diesel::insert_into(overrides) - .values(NewOverride { - key: msg.key, - value: msg.value, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching override"); - - overrides - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchAllOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching override"); - overrides.get_results::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self - .0 - .get() - .expect("Error on making DB connection for fetching override"); - - diesel::delete(overrides) - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} diff --git a/src/db/messages/context_overrides.rs b/src/db/messages/context_overrides.rs deleted file mode 100644 index 14a523ffd..000000000 --- a/src/db/messages/context_overrides.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::db::models::db_models::CtxOverrides; -use actix::Message; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateCtxOverrides { - pub context_id: String, - pub override_id: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchAllCtxOverrides; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchCtxOverrides { - pub context_id: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct DeleteCtxOverrides { - pub context_id: String, -} diff --git a/src/db/messages/dimensions.rs b/src/db/messages/dimensions.rs deleted file mode 100644 index 5d62f21ed..000000000 --- a/src/db/messages/dimensions.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::db::models::db_models::Dimension; -use actix::Message; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchDimensions; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchDimension { - pub dimension: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateDimension { - pub dimension: String, - pub priority: i32, -} diff --git a/src/db/messages/global_config.rs b/src/db/messages/global_config.rs deleted file mode 100644 index ea4d1d406..000000000 --- a/src/db/messages/global_config.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::db::models::db_models::GlobalConfig; -use actix::Message; -use diesel::QueryResult; -use serde_json::Value; - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchGlobalConfig; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchConfigKey { - pub key: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateGlobalKey { - pub key: String, - pub value: Value, -} diff --git a/src/db/messages/mod.rs b/src/db/messages/mod.rs deleted file mode 100644 index 5701bbda5..000000000 --- a/src/db/messages/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod context_overrides; -pub mod dimensions; -pub mod global_config; -pub mod new_contexts; -pub mod overrides; diff --git a/src/db/messages/new_contexts.rs b/src/db/messages/new_contexts.rs deleted file mode 100644 index dc897bf7c..000000000 --- a/src/db/messages/new_contexts.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::db::models::db_models::NewContexts; -use actix::Message; -use diesel::QueryResult; -use serde_json::Value; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateNewContext { - pub key: String, - pub value: Value, - pub column1: Option, - pub column2: Option, - pub column3: Option, - pub column4: Option, -} - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchAllNewContexts; - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchNewContext { - pub column1: Option, - pub column2: Option, - pub column3: Option, - pub column4: Option, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct DeleteNewContext { - pub key: String, -} diff --git a/src/db/messages/overrides.rs b/src/db/messages/overrides.rs deleted file mode 100644 index cfe5b13e3..000000000 --- a/src/db/messages/overrides.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::db::models::db_models::Overrides; -use actix::Message; -use diesel::QueryResult; -use serde_json::Value; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateOverride { - pub key: String, - pub value: Value, -} - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchAllOverrides; - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchOverride { - pub key: String, -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct DeleteOverride { - pub key: String, -} diff --git a/src/db/mod.rs b/src/db/mod.rs deleted file mode 100644 index 347009280..000000000 --- a/src/db/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod handlers; -pub mod messages; -pub mod models; -pub mod utils; -pub mod schema; diff --git a/src/db/models/db_models.rs b/src/db/models/db_models.rs deleted file mode 100644 index 7e0b78e21..000000000 --- a/src/db/models/db_models.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::db::schema::{contexts, ctxoverrides, dimensions, global_config, overrides}; -use chrono::offset::Utc; -use chrono::DateTime; -use diesel::{Identifiable, Queryable}; -use serde::Serialize; -use serde_json::Value; - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = dimensions)] -#[diesel(primary_key(dimension))] -pub struct Dimension { - pub dimension: String, - pub priority: i32, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = global_config)] -#[diesel(primary_key(key))] -pub struct GlobalConfig { - pub key: String, - pub value: Value, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = overrides)] -#[diesel(primary_key(key))] -pub struct Overrides { - pub key: String, - pub value: Value, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = ctxoverrides)] -#[diesel(primary_key(context_id))] -pub struct CtxOverrides { - pub context_id: String, - pub override_id: String, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = contexts)] -#[diesel(primary_key(key))] -pub struct NewContexts { - pub key: String, - pub value: Value, - pub column1: Option, - pub column2: Option, - pub column3: Option, - pub column4: Option, - pub last_modified: DateTime, - pub created_on: DateTime, -} diff --git a/src/db/models/insertables/context_overrides.rs b/src/db/models/insertables/context_overrides.rs deleted file mode 100644 index 634fef063..000000000 --- a/src/db/models/insertables/context_overrides.rs +++ /dev/null @@ -1,11 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; - -use crate::db::schema::ctxoverrides; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = ctxoverrides)] -pub struct CtxOverrideInsertion { - pub context_id: String, - pub override_id: String, -} diff --git a/src/db/models/insertables/contexts.rs b/src/db/models/insertables/contexts.rs deleted file mode 100644 index 7cab7238c..000000000 --- a/src/db/models/insertables/contexts.rs +++ /dev/null @@ -1,11 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; - -use crate::db::schema::contexts; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = contexts)] -pub struct NewContext { - pub key: String, - pub value: String, -} diff --git a/src/db/models/insertables/dimensions.rs b/src/db/models/insertables/dimensions.rs deleted file mode 100644 index 09c4caf6f..000000000 --- a/src/db/models/insertables/dimensions.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::db::schema::dimensions; -use diesel::Insertable; -use serde::Serialize; - -#[derive(Insertable, Serialize, Clone)] -#[diesel(table_name=dimensions)] -pub struct NewDimension { - pub dimension: String, - pub priority: i32, -} diff --git a/src/db/models/insertables/global_config.rs b/src/db/models/insertables/global_config.rs deleted file mode 100644 index 7d965d6d5..000000000 --- a/src/db/models/insertables/global_config.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::db::schema::global_config; -use diesel::Insertable; -use serde::Serialize; -use serde_json::Value; - -#[derive(Insertable, Serialize, Clone)] -#[diesel(table_name=global_config)] -pub struct NewGlobalConfigKey { - pub key: String, - pub value: Value, -} diff --git a/src/db/models/insertables/mod.rs b/src/db/models/insertables/mod.rs deleted file mode 100644 index 7b9cd61da..000000000 --- a/src/db/models/insertables/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod context_overrides; -pub mod contexts; -pub mod dimensions; -pub mod global_config; -pub mod new_contexts; -pub mod overrides; diff --git a/src/db/models/insertables/new_contexts.rs b/src/db/models/insertables/new_contexts.rs deleted file mode 100644 index ff40221f9..000000000 --- a/src/db/models/insertables/new_contexts.rs +++ /dev/null @@ -1,16 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; -use serde_json::Value; - -use crate::db::schema::newcontexts; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = newcontexts)] -pub struct NewContextInsertion { - pub key: String, - pub value: Value, - pub column1: Option, - pub column2: Option, - pub column3: Option, - pub column4: Option, -} diff --git a/src/db/models/insertables/overrides.rs b/src/db/models/insertables/overrides.rs deleted file mode 100644 index e216806bc..000000000 --- a/src/db/models/insertables/overrides.rs +++ /dev/null @@ -1,12 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; -use serde_json::Value; - -use crate::db::schema::overrides; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = overrides)] -pub struct NewOverride { - pub key: String, - pub value: Value, -} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs deleted file mode 100644 index 215ab33bc..000000000 --- a/src/db/models/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod db_models; -pub mod insertables; diff --git a/src/db/schema.rs b/src/db/schema.rs deleted file mode 100644 index 964a8e185..000000000 --- a/src/db/schema.rs +++ /dev/null @@ -1,68 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - contexts (key) { - key -> Varchar, - value -> Varchar, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - ctxoverrides (context_id) { - context_id -> Varchar, - override_id -> Varchar, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - global_config (key) { - key -> Varchar, - value -> Json, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - overrides (key) { - key -> Varchar, - value -> Json, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - newcontexts (key) { - key -> Varchar, - value -> Json, - column1 -> Nullable, - column2 -> Nullable, - column3 -> Nullable, - column4 -> Nullable, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::allow_tables_to_appear_in_same_query!( - contexts, - ctxoverrides, - dimensions, - global_config, - overrides, - newcontexts, -); diff --git a/src/main.rs b/src/main.rs index 7d741aca2..da448bb86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,16 @@ -mod api; -mod db; -mod utils; mod v1; -use api::primary::{ - context_overrides::{delete_ctx_override, get_ctx_override, post_ctx_override}, - dimensions::{get_dimension_key, get_dimensions, post_dimension}, - global_config::{get_global_config, get_global_config_key, post_config_key_value}, - overrides::{delete_override, get_override, post_override}, -}; - -use api::derived::{ - config::get_config, context_override::add_new_context_override, - promote::promote_contexts_overrides, reduce::reduce_contexts_overrides, -}; - use dotenv; use std::{env, io::Result}; -use actix::SyncArbiter; use actix_web::{ middleware::Logger, web::get, web::scope, web::Data, App, HttpResponse, HttpServer, }; -use db::utils::{get_pool, AppState, DbActor}; + +use service_utils::{ + db::utils::get_pool, + service::types::AppState, +}; use v1::{api::*, helpers::get_default_config_validation_schema}; @@ -31,14 +19,11 @@ async fn main() -> Result<()> { dotenv::dotenv().ok(); env_logger::init(); let pool = get_pool().await; - let pool_cl = pool.clone(); - let db_addr = SyncArbiter::start(5, move || DbActor(pool_cl.clone())); let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); HttpServer::new(move || { let logger: Logger = Logger::default(); App::new() .app_data(Data::new(AppState { - db: db_addr.clone(), db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), admin_token: admin_token.to_owned() @@ -48,36 +33,6 @@ async fn main() -> Result<()> { "/health", get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), ) - /***************************** Primary api routes *****************************/ - .service( - scope("/global_config") - .service(get_global_config) - .service(get_global_config_key) - .service(post_config_key_value), - ) - .service( - scope("/dimensions") - .service(get_dimensions) - .service(get_dimension_key) - .service(post_dimension), - ) - .service( - scope("/context_overrides") - .service(post_ctx_override) - .service(delete_ctx_override) - .service(get_ctx_override), - ) - .service( - scope("/override") - .service(post_override) - .service(delete_override) - .service(get_override), - ) - /***************************** Derived api routes *****************************/ - .service(scope("/config-old").service(get_config)) - .service(scope("add_context_overrides").service(add_new_context_override)) - .service(scope("reduce").service(reduce_contexts_overrides)) - .service(scope("promote").service(promote_contexts_overrides)) /***************************** V1 Routes *****************************/ .service(scope("/context").service(context::endpoints())) .service(scope("/dimension").service(dimension::endpoints())) diff --git a/src/utils/errors.rs b/src/utils/errors.rs deleted file mode 100644 index 2f160e7f6..000000000 --- a/src/utils/errors.rs +++ /dev/null @@ -1,85 +0,0 @@ -use actix_web::Either; -use actix_web::{ - error::ResponseError, - http::StatusCode, - Either::{Left, Right}, - HttpResponse, -}; -use serde::Serialize; -use serde_json::{to_value, Value}; -use std::fmt; - -#[derive(Debug, Clone, Serialize)] -pub enum AppErrorType { - DataExists, - DBError, - NotFound, - SomethingWentWrong, - BadRequest, -} - -#[derive(Debug)] -pub struct AppError { - pub message: Option, - pub cause: Option>, - pub status: AppErrorType, -} - -#[derive(Serialize)] -pub struct ErrorResponse { - status: AppErrorType, - message: String, - cause: Option, -} - -impl AppError { - fn message(&self) -> String { - match &*self { - AppError { - message: Some(message), - .. - } => message.clone(), - _ => "Reason not available".to_string(), - } - } - - fn cause(&self) -> Option { - match &*self { - AppError { cause, .. } => match cause { - Some(Left(val)) => to_value(val.clone()).ok(), - Some(Right(val)) => Some(val.clone()), - _ => None, - }, - } - } -} - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{:?}", self) - } -} - -impl ResponseError for AppError { - fn status_code(&self) -> StatusCode { - match self.status { - AppErrorType::SomethingWentWrong => StatusCode::INTERNAL_SERVER_ERROR, - AppErrorType::DBError => StatusCode::INTERNAL_SERVER_ERROR, - AppErrorType::NotFound => StatusCode::NOT_FOUND, - AppErrorType::BadRequest => StatusCode::BAD_REQUEST, - AppErrorType::DataExists => StatusCode::ALREADY_REPORTED, - } - } - - fn error_response(&self) -> HttpResponse { - let status_code = self.status_code(); - - let json_body = ErrorResponse { - message: self.message(), - cause: self.cause(), - status: self.status.clone(), - }; - - HttpResponse::build(status_code).json(json_body) - } -} diff --git a/src/utils/hash.rs b/src/utils/hash.rs deleted file mode 100644 index bcb5f2063..000000000 --- a/src/utils/hash.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - -pub fn string_based_b64_hash(obj: T) -> u64 -where - T: Hash, -{ - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() -} diff --git a/src/utils/helpers.rs b/src/utils/helpers.rs deleted file mode 100644 index 06b1b2b24..000000000 --- a/src/utils/helpers.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde_json::{from_value, to_value, Value}; -use std::collections::BTreeMap; - -pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { - let b_tree: &BTreeMap = &from_value(json).ok()?; - to_value(b_tree).ok() -} - -pub fn split_stringified_key_value_pair(input: &str) -> Vec> { - let conditions_vector_splits: Vec<&str> = input.split("&").collect(); - let mut conditions_vector: Vec> = conditions_vector_splits - .iter() - .map(|&x| x.split("=").collect()) - .collect(); - - conditions_vector.sort_by(|a, b| a[0].cmp(&b[0])); - - return conditions_vector; -} - -pub fn strip_double_quotes(str: &str) -> &str { - if str.starts_with("\"") && str.ends_with("\"") { - let len = str.len(); - return &str[1..len - 1]; - } - - str -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index b099e3361..000000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod errors; -pub mod hash; -pub mod helpers; -pub mod validations; diff --git a/src/utils/validations.rs b/src/utils/validations.rs deleted file mode 100644 index eadb4e0e6..000000000 --- a/src/utils/validations.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use serde_json::{to_value, Value}; - -fn are_both_of_same_type(a: &Value, b: &Value) -> bool { - (a.is_boolean() && b.is_boolean()) - || (a.is_number() && b.is_number()) - || (a.is_string() && b.is_string()) - || (a.is_array() && b.is_array()) - || (a.is_object() && b.is_object()) -} - -fn type_of(a: &Value) -> String { - if a.is_boolean() { - return "Boolean".to_string(); - } - - if a.is_number() { - return "Number".to_string(); - } - - if a.is_string() { - return "String".to_string(); - } - - if a.is_array() { - return "Array".to_string(); - } - - if a.is_null() { - return "Null".to_string(); - } - - return "Object".to_string(); -} - -fn create_type_mismatch_error( - default_value: &Value, - overriding_value: &Value, - path: &mut Vec, -) -> Value { - let mut error_map = HashMap::new(); - error_map.insert("Path".to_string(), path.join("/")); - error_map.insert("Expected type".to_string(), type_of(default_value)); - error_map.insert("Encountered type".to_string(), type_of(overriding_value)); - - let error_message = to_value(error_map).unwrap(); // .map_err(|_| ValidationErrors::ErrorMessageParsingError)?; - - error_message -} - -fn create_structure_mismatch_error(path: &mut Vec) -> Value { - let mut error_map = HashMap::new(); - - error_map.insert("Path".to_string(), path.join("/")); - if let Some(_) = path.last() { - error_map.insert( - "Reason".to_string(), - "Key not found in default config".to_string(), - ); - } - - let error_message = to_value(error_map).unwrap(); // .map_err(|_| ValidationErrors::ErrorMessageParsingError)?; - - error_message -} - -fn validate_sub_tree_helper( - default_tree: &Value, - overriding_tree: &Value, - path: &mut Vec, -) -> Result { - if !are_both_of_same_type(default_tree, overriding_tree) { - let error_message = create_type_mismatch_error(default_tree, overriding_tree, path); - return Err(error_message); - } - - if !default_tree.is_object() { - return Ok(true); - } - - let subtree = overriding_tree.as_object().unwrap(); - - for (key, overriden_value) in (&*subtree).iter() { - let default_value = default_tree.get(key); - - path.push((&key).to_string()); - - let result = match default_value { - Some(val) => validate_sub_tree_helper(val, overriden_value, path), - None => Err(create_structure_mismatch_error(path)), - }?; - - if !result { - return Ok(false); - } - } - - return Ok(true); -} - -pub fn validate_sub_tree(default_tree: &Value, overriding_tree: &Value) -> Result { - validate_sub_tree_helper(default_tree, overriding_tree, &mut vec!["".to_string()]) -} - -// pub fn just_for_test() { - -// let default_json: Value = from_str(r#" -// { "package_dependencies": { -// "in.juspay.dotp": { -// "entry": "base.html", -// "root": "payments/in.juspay.dotp/" -// }, -// "in.juspay.escrow": { -// "entry": "base.html", -// "root": "payments/in.juspay.escrow/", -// "hello": { -// "test": 123 -// } -// } -// } -// } -// "#).unwrap(); - -// let overridden_json: Value = from_str(r#" -// { "package_dependencies": { -// "in.juspay.escrow": { -// "entry": "base.html", -// "hello": { -// "test1": 123 -// } -// } -// } -// } -// "#).unwrap(); - -// let (ans, reason) = validate_sub_tree(&default_json, &overridden_json); -// println!("Result : {}", ans); -// println!("Reason : {}", reason); - -// } diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index f77076c61..9412cb614 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -1,11 +1,13 @@ use super::types::Config; use crate::{ - db::utils::AppState, v1::{ db::schema::cac_v1::{contexts::dsl as ctxt, default_configs::dsl as def_conf}, - helpers::ToActixErr, }, }; +use service_utils::{ + service::types::AppState, + helpers::ToActixErr, +}; use actix_web::{ get, web::{Data, Json}, diff --git a/src/v1/api/context/handlers.rs b/src/v1/api/context/handlers.rs index 1c34aa929..6f13e9ab6 100644 --- a/src/v1/api/context/handlers.rs +++ b/src/v1/api/context/handlers.rs @@ -1,17 +1,18 @@ use crate::{ - db::utils::AppState, v1::{ api::{ context::types::{PaginationParams, PutReq, PutResp}, - types::AuthenticationInfo, }, db::{ models::{Context, Dimension}, schema::cac_v1::{contexts, dimensions::dsl::dimensions}, }, - helpers::ToActixErr, }, }; +use service_utils::{ + service::types::{AppState, AuthenticationInfo}, + helpers::ToActixErr, +}; use actix_web::{ delete, error::{self, ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, diff --git a/src/v1/api/default_config/handlers.rs b/src/v1/api/default_config/handlers.rs index 47a4ade69..ff7af7e3d 100644 --- a/src/v1/api/default_config/handlers.rs +++ b/src/v1/api/default_config/handlers.rs @@ -1,14 +1,13 @@ use super::helpers::validate_schema; use super::types::CreateReq; use crate::{ - db::utils::AppState, v1::{ - api::types::AuthenticationInfo, db::{ models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs, }, }, }; +use service_utils::service::types::{AppState, AuthenticationInfo}; use actix_web::{ put, web::{self, Data}, diff --git a/src/v1/api/dimension/handlers.rs b/src/v1/api/dimension/handlers.rs index fcc1b1a57..681594c19 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/src/v1/api/dimension/handlers.rs @@ -1,10 +1,12 @@ use crate::{ - db::utils::AppState, v1::{ - api::{dimension::types::CreateReq, types::AuthenticationInfo}, + api::{dimension::types::CreateReq}, db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, }, }; +use service_utils::{ + service::types::{AppState, AuthenticationInfo}, +}; use actix_web::{ put, web::{self, Data}, diff --git a/src/v1/api/mod.rs b/src/v1/api/mod.rs index 98aaf28ad..654a075a0 100644 --- a/src/v1/api/mod.rs +++ b/src/v1/api/mod.rs @@ -2,4 +2,3 @@ pub mod context; pub mod dimension; pub mod default_config; pub mod config; -pub mod types; diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs index ea38fd114..e280ab4c8 100644 --- a/src/v1/helpers.rs +++ b/src/v1/helpers.rs @@ -1,46 +1,6 @@ -use std::{env::VarError, fmt, str::FromStr}; - -use actix_web::{error::ErrorInternalServerError, Error}; use jsonschema::{Draft, JSONSchema}; use serde_json::json; -//WARN Do NOT use this fxn inside api requests, instead add the required -//env to AppState and get value from there. As this panics, it should -//only be used for envs needed during app start. -pub fn get_from_env_unsafe(name: &str) -> Result -where - F: FromStr, - ::Err: std::fmt::Debug, -{ - std::env::var(name) - .map(|val| val.parse().unwrap()) - .map_err(|e| { - println!("{name} env not found with error: {e}"); - return e; - }) -} - -pub trait ToActixErr { - fn map_err_to_internal_server(self, log_prefix: &str, err_body: B) -> Result - where - B: fmt::Debug + fmt::Display + 'static; -} - -impl ToActixErr for Result -where - E: fmt::Debug, -{ - fn map_err_to_internal_server(self, log_prefix: &str, err_body: B) -> Result - where - B: fmt::Debug + fmt::Display + 'static, - { - self.map_err(|e| { - println!("{log_prefix}, err: {e:?}"); - ErrorInternalServerError(err_body) - }) - } -} - pub fn get_default_config_validation_schema() -> JSONSchema { let my_schema = json!({ "type": "object", diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 4d9b03d74..8b387661d 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -1,4 +1,3 @@ pub mod api; -pub mod aws; pub mod db; pub mod helpers; From 76cabf47549cf5a9e196fa9550ec17f1a705eb47 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Wed, 26 Jul 2023 18:31:32 +0530 Subject: [PATCH 053/352] feat: added 304 <> last-modified for GET /config --- src/v1/api/config/handlers.rs | 45 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/v1/api/config/handlers.rs b/src/v1/api/config/handlers.rs index 9412cb614..d8c798d31 100644 --- a/src/v1/api/config/handlers.rs +++ b/src/v1/api/config/handlers.rs @@ -1,31 +1,42 @@ use super::types::Config; -use crate::{ - v1::{ - db::schema::cac_v1::{contexts::dsl as ctxt, default_configs::dsl as def_conf}, - }, +use crate::v1::db::schema::cac_v1::{ + contexts::dsl as ctxt, default_configs::dsl as def_conf, }; -use service_utils::{ - service::types::AppState, - helpers::ToActixErr, -}; -use actix_web::{ - get, - web::{Data, Json}, - Scope, -}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use actix_web::{get, web::Data, HttpRequest, HttpResponse, Scope}; +use chrono::{DateTime, Utc}; +use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value, Value::Null}; +use service_utils::{helpers::ToActixErr, service::types::AppState}; pub fn endpoints() -> Scope { Scope::new("").service(get) } #[get("")] -async fn get(state: Data) -> actix_web::Result> { +async fn get(req: HttpRequest, state: Data) -> actix_web::Result { let mut conn = state .db_pool .get() .map_err_to_internal_server("error getting a connection from db pool", Null)?; + let max_created_at: Option> = ctxt::contexts + .select(max(ctxt::created_at)) + .first(&mut conn) + .map_err_to_internal_server("error getting created at", Null)?; + + let last_modified = req + .headers() + .get("If-Modified-Since") + .and_then(|header_val| { + let header_str = header_val.to_str().ok()?; + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc)) + .ok() + }); + + if max_created_at.is_some() && max_created_at < last_modified + { + return Ok(HttpResponse::NotModified().finish()); + }; let contexts_vec = ctxt::contexts .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) @@ -37,7 +48,7 @@ async fn get(state: Data) -> actix_web::Result> { (Vec::new(), Map::new()), |(mut ctxts, mut overrides), (id, condition, override_id, override_)| { let ctxt = super::types::Context { - id: id, + id, condition, override_with_keys: [override_id.to_owned()], }; @@ -66,5 +77,5 @@ async fn get(state: Data) -> actix_web::Result> { default_configs, }; - Ok(Json(config)) + return Ok(HttpResponse::Ok().json(config)); } From afbf7229588b78faa877618ebd09fd367baefca1 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 27 Jul 2023 23:05:19 +0530 Subject: [PATCH 054/352] refactor: moved cac to cargo workspaces --- .gitignore | 2 + Cargo.toml | 46 +- crates/context-aware-config/Cargo.toml | 42 + .../context-aware-config/diesel.toml | 2 +- .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../2023-05-24-134019_create_contexts/up.sql | 0 .../down.sql | 0 .../2023-05-25-111645_create_overrides/up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../samples}/default_config.json | 0 .../samples}/dimensions.json | 0 .../samples}/overrides.json | 0 .../src}/api/config/handlers.rs | 5 +- .../src}/api/config/mod.rs | 0 .../src}/api/config/types.rs | 0 .../src}/api/context/handlers.rs | 24 +- .../src}/api/context/mod.rs | 0 .../src}/api/context/types.rs | 4 +- .../src}/api/default_config/handlers.rs | 10 +- .../src}/api/default_config/helpers.rs | 5 +- .../src}/api/default_config/mod.rs | 2 +- .../src}/api/default_config/types.rs | 4 +- .../src}/api/dimension/handlers.rs | 10 +- .../src}/api/dimension/mod.rs | 0 .../src}/api/dimension/types.rs | 2 +- .../context-aware-config/src}/api/mod.rs | 4 +- .../context-aware-config/src}/db/mod.rs | 0 .../context-aware-config/src}/db/models.rs | 6 +- .../context-aware-config/src}/db/schema.rs | 0 crates/context-aware-config/src/helpers.rs | 40 + .../context-aware-config/src}/main.rs | 14 +- .../context-aware-config/test}/index.js | 0 crates/service-utils/Cargo.lock | 2528 ----------------- makefile | 2 +- migrations/.keep | 0 package-lock.json | 2 +- postman/cac/config/Get Config/event.test.js | 9 +- src/v1/helpers.rs | 40 - src/v1/mod.rs | 3 - 56 files changed, 140 insertions(+), 2666 deletions(-) create mode 100644 crates/context-aware-config/Cargo.toml rename diesel.toml => crates/context-aware-config/diesel.toml (73%) rename {migrations => crates/context-aware-config/migrations}/00000000000000_diesel_initial_setup/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/00000000000000_diesel_initial_setup/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/00000000000000_diesel_initial_setup/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/00000000000000_diesel_initial_setup/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-24-134019_create_contexts/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-24-134019_create_contexts/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-25-111645_create_overrides/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-25-111645_create_overrides/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-29-140304_create_dimensions/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-05-29-140304_create_dimensions/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-12-070734_add_priority_to_contexts/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-12-070734_add_priority_to_contexts/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-14-054049_create-default_configs/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-14-054049_create-default_configs/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-19-130600_add_schema_to_default_config/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-19-130600_add_schema_to_default_config/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql (100%) rename {migrations => crates/context-aware-config/migrations}/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql (100%) rename {samples => crates/context-aware-config/samples}/default_config.json (100%) rename {samples => crates/context-aware-config/samples}/dimensions.json (100%) rename {samples => crates/context-aware-config/samples}/overrides.json (100%) rename {src/v1 => crates/context-aware-config/src}/api/config/handlers.rs (98%) rename {src/v1 => crates/context-aware-config/src}/api/config/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/api/config/types.rs (100%) rename {src/v1 => crates/context-aware-config/src}/api/context/handlers.rs (96%) rename {src/v1 => crates/context-aware-config/src}/api/context/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/api/context/types.rs (86%) rename {src/v1 => crates/context-aware-config/src}/api/default_config/handlers.rs (94%) rename {src/v1 => crates/context-aware-config/src}/api/default_config/helpers.rs (84%) rename {src/v1 => crates/context-aware-config/src}/api/default_config/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/api/default_config/types.rs (59%) rename {src/v1 => crates/context-aware-config/src}/api/dimension/handlers.rs (88%) rename {src/v1 => crates/context-aware-config/src}/api/dimension/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/api/dimension/types.rs (78%) rename {src/v1 => crates/context-aware-config/src}/api/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/db/mod.rs (100%) rename {src/v1 => crates/context-aware-config/src}/db/models.rs (87%) rename {src/v1 => crates/context-aware-config/src}/db/schema.rs (100%) create mode 100644 crates/context-aware-config/src/helpers.rs rename {src => crates/context-aware-config/src}/main.rs (85%) rename {test => crates/context-aware-config/test}/index.js (100%) delete mode 100644 crates/service-utils/Cargo.lock delete mode 100644 migrations/.keep delete mode 100644 src/v1/helpers.rs delete mode 100644 src/v1/mod.rs diff --git a/.gitignore b/.gitignore index 047e09de4..565e77763 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ backend/.env bacon.toml docker-compose/localstack/export_cyphers.sh test_logs + +.keep \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6dd3590d8..68417c118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,42 +1,6 @@ -[package] -name = "context-aware-config" -version = "0.1.0" -edition = "2021" +[workspace] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# env -dotenv = "0.15.0" -# Https server framework -actix = "0.13.0" -actix-web = "4.0.0" -# To help with generating uuids -uuid = {version = "^0.8", features = ["v4","serde"]} -# To serialize and deserialize objects from json -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} -# For logging and debugging -env_logger = "0.8" -log = "^0.4" -# to work with enums -strum_macros = "^0.24" -strum = {version = "^0.24"} -derive_more = "^0.99" -# date and time -chrono = { version = "0.4", features = ["serde"] } -# ORM -diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } -blake3 = "1.3.3" -rusoto_kms = "0.48.0" -rusoto_signature = "0.48.0" -bytes = "1.4.0" -rusoto_core = "0.48.0" -base64 = "0.21.2" -diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } -urlencoding = "~2.1.2" -jsonschema = "~0.17" -reqwest = { version = "*", features = [ "rustls-tls" ] } -json-patch = "1.0.0" - -service-utils = { path = "crates/service-utils" } +members = [ + "crates/service-utils", + "crates/context-aware-config", +] diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml new file mode 100644 index 000000000..931f69d63 --- /dev/null +++ b/crates/context-aware-config/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "context-aware-config" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = "0.15.0" +# Https server framework +actix = "0.13.0" +actix-web = "4.0.0" +# To help with generating uuids +uuid = {version = "^0.8", features = ["v4","serde"]} +# To serialize and deserialize objects from json +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +# For logging and debugging +env_logger = "0.8" +log = "^0.4" +# to work with enums +strum_macros = "^0.24" +strum = {version = "^0.24"} +derive_more = "^0.99" +# date and time +chrono = { version = "0.4", features = ["serde"] } +# ORM +diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +blake3 = "1.3.3" +rusoto_kms = "0.48.0" +rusoto_signature = "0.48.0" +bytes = "1.4.0" +rusoto_core = "0.48.0" +base64 = "0.21.2" +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = "~2.1.2" +jsonschema = "~0.17" +reqwest = { version = "*", features = [ "rustls-tls" ] } +json-patch = "1.0.0" + +service-utils = { path = "../service-utils" } diff --git a/diesel.toml b/crates/context-aware-config/diesel.toml similarity index 73% rename from diesel.toml rename to crates/context-aware-config/diesel.toml index c4c7de248..6192f8dbf 100644 --- a/diesel.toml +++ b/crates/context-aware-config/diesel.toml @@ -1,5 +1,5 @@ [print_schema] -file = "src/v1/db/schema.rs" +file = "src/db/schema.rs" schema = "cac_v1" [migrations_directory] diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from migrations/00000000000000_diesel_initial_setup/down.sql rename to crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from migrations/00000000000000_diesel_initial_setup/up.sql rename to crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql diff --git a/migrations/v1/00000000000000_diesel_initial_setup/down.sql b/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from migrations/v1/00000000000000_diesel_initial_setup/down.sql rename to crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql diff --git a/migrations/v1/00000000000000_diesel_initial_setup/up.sql b/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from migrations/v1/00000000000000_diesel_initial_setup/up.sql rename to crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql diff --git a/migrations/v1/2023-05-24-134019_create_contexts/down.sql b/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql similarity index 100% rename from migrations/v1/2023-05-24-134019_create_contexts/down.sql rename to crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql diff --git a/migrations/v1/2023-05-24-134019_create_contexts/up.sql b/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql similarity index 100% rename from migrations/v1/2023-05-24-134019_create_contexts/up.sql rename to crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql diff --git a/migrations/v1/2023-05-25-111645_create_overrides/down.sql b/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql similarity index 100% rename from migrations/v1/2023-05-25-111645_create_overrides/down.sql rename to crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql diff --git a/migrations/v1/2023-05-25-111645_create_overrides/up.sql b/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql similarity index 100% rename from migrations/v1/2023-05-25-111645_create_overrides/up.sql rename to crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql diff --git a/migrations/v1/2023-05-29-140304_create_dimensions/down.sql b/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql similarity index 100% rename from migrations/v1/2023-05-29-140304_create_dimensions/down.sql rename to crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql diff --git a/migrations/v1/2023-05-29-140304_create_dimensions/up.sql b/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql similarity index 100% rename from migrations/v1/2023-05-29-140304_create_dimensions/up.sql rename to crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql diff --git a/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql b/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql similarity index 100% rename from migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql rename to crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql diff --git a/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql b/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql similarity index 100% rename from migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql rename to crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql diff --git a/migrations/v1/2023-06-14-054049_create-default_configs/down.sql b/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql similarity index 100% rename from migrations/v1/2023-06-14-054049_create-default_configs/down.sql rename to crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql diff --git a/migrations/v1/2023-06-14-054049_create-default_configs/up.sql b/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql similarity index 100% rename from migrations/v1/2023-06-14-054049_create-default_configs/up.sql rename to crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql diff --git a/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql b/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql similarity index 100% rename from migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql rename to crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql diff --git a/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql b/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql similarity index 100% rename from migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql rename to crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql diff --git a/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql b/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql similarity index 100% rename from migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql rename to crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql diff --git a/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql b/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql similarity index 100% rename from migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql rename to crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql diff --git a/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql b/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql similarity index 100% rename from migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql rename to crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql diff --git a/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql b/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql similarity index 100% rename from migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql rename to crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql diff --git a/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql b/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql similarity index 100% rename from migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql rename to crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql diff --git a/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql b/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql similarity index 100% rename from migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql rename to crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql diff --git a/samples/default_config.json b/crates/context-aware-config/samples/default_config.json similarity index 100% rename from samples/default_config.json rename to crates/context-aware-config/samples/default_config.json diff --git a/samples/dimensions.json b/crates/context-aware-config/samples/dimensions.json similarity index 100% rename from samples/dimensions.json rename to crates/context-aware-config/samples/dimensions.json diff --git a/samples/overrides.json b/crates/context-aware-config/samples/overrides.json similarity index 100% rename from samples/overrides.json rename to crates/context-aware-config/samples/overrides.json diff --git a/src/v1/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs similarity index 98% rename from src/v1/api/config/handlers.rs rename to crates/context-aware-config/src/api/config/handlers.rs index d8c798d31..e89b185f3 100644 --- a/src/v1/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,5 +1,5 @@ use super::types::Config; -use crate::v1::db::schema::cac_v1::{ +use crate::db::schema::cac_v1::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, }; use actix_web::{get, web::Data, HttpRequest, HttpResponse, Scope}; @@ -33,8 +33,7 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result Scope { Scope::new("") @@ -218,7 +214,7 @@ async fn get_context( path: web::Path, state: Data, ) -> Result { - use crate::v1::db::schema::cac_v1::contexts::dsl::*; + use crate::db::schema::cac_v1::contexts::dsl::*; let ctx_id = path.into_inner(); let mut conn = match state.db_pool.get() { @@ -251,7 +247,7 @@ async fn list_contexts( qparams: web::Query, state: Data, ) -> Result { - use crate::v1::db::schema::cac_v1::contexts::dsl::*; + use crate::db::schema::cac_v1::contexts::dsl::*; let mut conn = state .db_pool diff --git a/src/v1/api/context/mod.rs b/crates/context-aware-config/src/api/context/mod.rs similarity index 100% rename from src/v1/api/context/mod.rs rename to crates/context-aware-config/src/api/context/mod.rs diff --git a/src/v1/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs similarity index 86% rename from src/v1/api/context/types.rs rename to crates/context-aware-config/src/api/context/types.rs index d3dad705d..e0c81e855 100644 --- a/src/v1/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use serde_json::{ Value, Map}; +use serde_json::{Map, Value}; #[derive(Deserialize)] pub struct PutReq { @@ -17,5 +17,5 @@ pub struct PutResp { #[derive(Deserialize)] pub struct PaginationParams { pub page: Option, - pub size: Option + pub size: Option, } diff --git a/src/v1/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs similarity index 94% rename from src/v1/api/default_config/handlers.rs rename to crates/context-aware-config/src/api/default_config/handlers.rs index ff7af7e3d..20eaccad5 100644 --- a/src/v1/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,13 +1,8 @@ use super::helpers::validate_schema; use super::types::CreateReq; -use crate::{ - v1::{ - db::{ - models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs, - }, - }, +use crate::db::{ + models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs, }; -use service_utils::service::types::{AppState, AuthenticationInfo}; use actix_web::{ put, web::{self, Data}, @@ -17,6 +12,7 @@ use chrono::Utc; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; +use service_utils::service::types::{AppState, AuthenticationInfo}; pub fn endpoints() -> Scope { Scope::new("").service(create) diff --git a/src/v1/api/default_config/helpers.rs b/crates/context-aware-config/src/api/default_config/helpers.rs similarity index 84% rename from src/v1/api/default_config/helpers.rs rename to crates/context-aware-config/src/api/default_config/helpers.rs index b4b1a7212..8ee2119ba 100644 --- a/src/v1/api/default_config/helpers.rs +++ b/crates/context-aware-config/src/api/default_config/helpers.rs @@ -7,7 +7,10 @@ use serde_json::Value; validations for the input. */ // TODO: Recursive validation. -pub fn validate_schema(validation_schema: &JSONSchema, schema: Value) -> Result<(), String> { +pub fn validate_schema( + validation_schema: &JSONSchema, + schema: Value, +) -> Result<(), String> { let res = match validation_schema.validate(&schema) { Ok(_) => Ok(()), Err(e) => { diff --git a/src/v1/api/default_config/mod.rs b/crates/context-aware-config/src/api/default_config/mod.rs similarity index 100% rename from src/v1/api/default_config/mod.rs rename to crates/context-aware-config/src/api/default_config/mod.rs index 25f79f70b..fa13772fb 100644 --- a/src/v1/api/default_config/mod.rs +++ b/crates/context-aware-config/src/api/default_config/mod.rs @@ -1,4 +1,4 @@ mod handlers; -mod types; mod helpers; +mod types; pub use handlers::endpoints; diff --git a/src/v1/api/default_config/types.rs b/crates/context-aware-config/src/api/default_config/types.rs similarity index 59% rename from src/v1/api/default_config/types.rs rename to crates/context-aware-config/src/api/default_config/types.rs index 2aae54cb5..9bc4826a8 100644 --- a/src/v1/api/default_config/types.rs +++ b/crates/context-aware-config/src/api/default_config/types.rs @@ -1,8 +1,8 @@ use serde::Deserialize; -use serde_json::{Value, Map}; +use serde_json::{Map, Value}; #[derive(Deserialize)] pub struct CreateReq { pub value: Value, - pub schema: Map + pub schema: Map, } diff --git a/src/v1/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs similarity index 88% rename from src/v1/api/dimension/handlers.rs rename to crates/context-aware-config/src/api/dimension/handlers.rs index 681594c19..571f1d116 100644 --- a/src/v1/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -1,11 +1,6 @@ use crate::{ - v1::{ - api::{dimension::types::CreateReq}, - db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, - }, -}; -use service_utils::{ - service::types::{AppState, AuthenticationInfo}, + api::dimension::types::CreateReq, + db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, }; use actix_web::{ put, @@ -14,6 +9,7 @@ use actix_web::{ }; use chrono::Utc; use diesel::RunQueryDsl; +use service_utils::service::types::{AppState, AuthenticationInfo}; pub fn endpoints() -> Scope { Scope::new("").service(create) diff --git a/src/v1/api/dimension/mod.rs b/crates/context-aware-config/src/api/dimension/mod.rs similarity index 100% rename from src/v1/api/dimension/mod.rs rename to crates/context-aware-config/src/api/dimension/mod.rs diff --git a/src/v1/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs similarity index 78% rename from src/v1/api/dimension/types.rs rename to crates/context-aware-config/src/api/dimension/types.rs index 494df71eb..336210289 100644 --- a/src/v1/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use crate::v1::db::models::DimensionType; +use crate::db::models::DimensionType; #[derive(Deserialize)] pub struct CreateReq { diff --git a/src/v1/api/mod.rs b/crates/context-aware-config/src/api/mod.rs similarity index 100% rename from src/v1/api/mod.rs rename to crates/context-aware-config/src/api/mod.rs index 654a075a0..32c456945 100644 --- a/src/v1/api/mod.rs +++ b/crates/context-aware-config/src/api/mod.rs @@ -1,4 +1,4 @@ +pub mod config; pub mod context; -pub mod dimension; pub mod default_config; -pub mod config; +pub mod dimension; diff --git a/src/v1/db/mod.rs b/crates/context-aware-config/src/db/mod.rs similarity index 100% rename from src/v1/db/mod.rs rename to crates/context-aware-config/src/db/mod.rs diff --git a/src/v1/db/models.rs b/crates/context-aware-config/src/db/models.rs similarity index 87% rename from src/v1/db/models.rs rename to crates/context-aware-config/src/db/models.rs index d78a8b213..16f8febc8 100644 --- a/src/v1/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -1,4 +1,4 @@ -use crate::v1::db::schema::cac_v1::*; +use crate::db::schema::cac_v1::*; use chrono::offset::Utc; use chrono::DateTime; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; @@ -20,9 +20,9 @@ pub struct Context { } #[derive(Debug, Clone, Copy, diesel_derive_enum::DbEnum, Deserialize, Serialize)] -#[ExistingTypePath = "crate::v1::db::schema::cac_v1::sql_types::DimensionType"] +#[ExistingTypePath = "crate::db::schema::cac_v1::sql_types::DimensionType"] #[DbValueStyle = "UPPERCASE"] -#[ExistingTypePath = "crate::v1::db::schema::sql_types::DimensionType"] +#[ExistingTypePath = "crate::db::schema::sql_types::DimensionType"] pub enum DimensionType { BOOL, NUMBER, diff --git a/src/v1/db/schema.rs b/crates/context-aware-config/src/db/schema.rs similarity index 100% rename from src/v1/db/schema.rs rename to crates/context-aware-config/src/db/schema.rs diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs new file mode 100644 index 000000000..dbe00f3ef --- /dev/null +++ b/crates/context-aware-config/src/helpers.rs @@ -0,0 +1,40 @@ +use jsonschema::{Draft, JSONSchema}; +use serde_json::json; + +pub fn get_default_config_validation_schema() -> JSONSchema { + let my_schema = json!({ + "type": "object", + "properties": { + "type": { + "enum": ["string", "object", "enum", "number", "boolean", "array"] + } + }, + "required": ["type"], + "allOf": [ + // { + // "if": { + // "properties": { "type": { "const": "object" } } + // }, + // "then": { + // "properties": { "properties": { "type": "object" } }, + // "required": ["properties"] + // } + // }, + { + "if": { + "properties": { "type": { "const": "string" } } + }, + "then": { + "properties": { "pattern": { "type": "string" } }, + "required": ["pattern"] + } + }, + // TODO: Add validations for Array types. + ] + }); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&my_schema) + .expect("THE IMPOSSIBLE HAPPENED, failed to compile the schema for the schema!") +} diff --git a/src/main.rs b/crates/context-aware-config/src/main.rs similarity index 85% rename from src/main.rs rename to crates/context-aware-config/src/main.rs index da448bb86..7c433030d 100644 --- a/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -1,4 +1,6 @@ -mod v1; +mod api; +mod db; +mod helpers; use dotenv; use std::{env, io::Result}; @@ -7,12 +9,10 @@ use actix_web::{ middleware::Logger, web::get, web::scope, web::Data, App, HttpResponse, HttpServer, }; -use service_utils::{ - db::utils::get_pool, - service::types::AppState, -}; +use service_utils::{db::utils::get_pool, service::types::AppState}; -use v1::{api::*, helpers::get_default_config_validation_schema}; +use api::*; +use helpers::get_default_config_validation_schema; #[actix_web::main] async fn main() -> Result<()> { @@ -26,7 +26,7 @@ async fn main() -> Result<()> { .app_data(Data::new(AppState { db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), - admin_token: admin_token.to_owned() + admin_token: admin_token.to_owned(), })) .wrap(logger) .route( diff --git a/test/index.js b/crates/context-aware-config/test/index.js similarity index 100% rename from test/index.js rename to crates/context-aware-config/test/index.js diff --git a/crates/service-utils/Cargo.lock b/crates/service-utils/Cargo.lock deleted file mode 100644 index 14fb97360..000000000 --- a/crates/service-utils/Cargo.lock +++ /dev/null @@ -1,2528 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" -dependencies = [ - "actix-rt", - "actix_derive", - "bitflags 1.3.2", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" -dependencies = [ - "bitflags 1.3.2", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash 0.8.3", - "base64 0.21.2", - "bitflags 1.3.2", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.27", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash 0.7.6", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "actix_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "addr2line" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "serde", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" - -[[package]] -name = "anstyle-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" - -[[package]] -name = "async-trait" -version = "0.1.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "bytecount" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "bytestring" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "winapi", -] - -[[package]] -name = "clap" -version = "4.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "diesel" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" -dependencies = [ - "bitflags 2.3.3", - "byteorder", - "chrono", - "diesel_derives", - "itoa", - "pq-sys", - "r2d2", - "serde_json", - "uuid", -] - -[[package]] -name = "diesel_derives" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.27", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set", - "regex", -] - -[[package]] -name = "fastrand" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" - -[[package]] -name = "h2" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "ipnet" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - -[[package]] -name = "iso8601" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" -dependencies = [ - "nom", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonschema" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" -dependencies = [ - "ahash 0.8.3", - "anyhow", - "base64 0.21.2", - "bytecount", - "clap", - "fancy-regex", - "fraction", - "getrandom", - "iso8601", - "itoa", - "memchr", - "num-cmp", - "once_cell", - "parking_lot", - "percent-encoding", - "regex", - "reqwest", - "serde", - "serde_json", - "time", - "url", - "uuid", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" - -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "md-5" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[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-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "openssl" -version = "0.10.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pin-project-lite" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "pq-sys" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" -dependencies = [ - "vcpkg", -] - -[[package]] -name = "proc-macro2" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rusoto_core" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" -dependencies = [ - "async-trait", - "base64 0.13.1", - "bytes", - "crc32fast", - "futures", - "http", - "hyper", - "hyper-tls", - "lazy_static", - "log", - "rusoto_credential", - "rusoto_signature", - "rustc_version", - "serde", - "serde_json", - "tokio", - "xml-rs", -] - -[[package]] -name = "rusoto_credential" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" -dependencies = [ - "async-trait", - "chrono", - "dirs-next", - "futures", - "hyper", - "serde", - "serde_json", - "shlex", - "tokio", - "zeroize", -] - -[[package]] -name = "rusoto_kms" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" -dependencies = [ - "async-trait", - "bytes", - "futures", - "rusoto_core", - "serde", - "serde_json", -] - -[[package]] -name = "rusoto_signature" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" -dependencies = [ - "base64 0.13.1", - "bytes", - "chrono", - "digest 0.9.0", - "futures", - "hex", - "hmac", - "http", - "hyper", - "log", - "md-5", - "percent-encoding", - "pin-project-lite", - "rusoto_credential", - "rustc_version", - "serde", - "sha2", - "tokio", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" -dependencies = [ - "bitflags 2.3.3", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" - -[[package]] -name = "serde" -version = "1.0.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "serde_json" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "service-utils" -version = "0.1.0" -dependencies = [ - "actix", - "actix-web", - "base64 0.21.2", - "bytes", - "diesel", - "dotenv", - "jsonschema", - "rusoto_core", - "rusoto_kms", - "rusoto_signature", - "urlencoding", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys", -] - -[[package]] -name = "thiserror" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "time" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" -dependencies = [ - "autocfg", - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "uuid" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.27", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "xml-rs" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" - -[[package]] -name = "zeroize" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" - -[[package]] -name = "zstd" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "6.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/makefile b/makefile index 5cbfcfa68..b15ae7be7 100644 --- a/makefile +++ b/makefile @@ -34,7 +34,7 @@ run: #populate the kms keyId sleep 10 #TODO move this sleep to aws cli list-keys command instead cargo build --color always - diesel migration run + diesel migration run --config-file=crates/context-aware-config/diesel.toml source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always diff --git a/migrations/.keep b/migrations/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/package-lock.json b/package-lock.json index 06ca68709..f7ae828c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -827,7 +827,7 @@ }, "node_modules/newmandir": { "version": "0.0.1", - "resolved": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git#82838d888737b1c1410813cbee0b2bda2c325a5e", + "resolved": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git#f9b700676e72c601e60793a8a0e083c5b06800cf", "dev": true, "license": "ISC", "dependencies": { diff --git a/postman/cac/config/Get Config/event.test.js b/postman/cac/config/Get Config/event.test.js index f57077057..bb89bec7a 100644 --- a/postman/cac/config/Get Config/event.test.js +++ b/postman/cac/config/Get Config/event.test.js @@ -1,3 +1,10 @@ pm.test("200 check", function() { pm.response.to.have.status(200); -}) \ No newline at end of file + let response = pm.response.json(); + let expected_response = { + "contexts": [], + "overrides": {}, + "default_configs": {} + }; + pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response)); +}) diff --git a/src/v1/helpers.rs b/src/v1/helpers.rs deleted file mode 100644 index e280ab4c8..000000000 --- a/src/v1/helpers.rs +++ /dev/null @@ -1,40 +0,0 @@ -use jsonschema::{Draft, JSONSchema}; -use serde_json::json; - -pub fn get_default_config_validation_schema() -> JSONSchema { - let my_schema = json!({ - "type": "object", - "properties": { - "type": { - "enum": ["string", "object", "enum", "number", "boolean", "array"] - } - }, - "required": ["type"], - "allOf": [ -// { -// "if": { -// "properties": { "type": { "const": "object" } } -// }, -// "then": { -// "properties": { "properties": { "type": "object" } }, -// "required": ["properties"] -// } -// }, - { - "if": { - "properties": { "type": { "const": "string" } } - }, - "then": { - "properties": { "pattern": { "type": "string" } }, - "required": ["pattern"] - } - }, - // TODO: Add validations for Array types. - ] - }); - - JSONSchema::options() - .with_draft(Draft::Draft7) - .compile(&my_schema) - .expect("THE IMPOSSIBLE HAPPENED, failed to compile the schema for the schema!") -} diff --git a/src/v1/mod.rs b/src/v1/mod.rs deleted file mode 100644 index 8b387661d..000000000 --- a/src/v1/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod api; -pub mod db; -pub mod helpers; From c77a2c2463dbd69e1dfa4018826a5a716d5ffd5a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 31 Jul 2023 20:11:05 +0530 Subject: [PATCH 055/352] refactor: moved fetching db connection in FromRequest trait impl --- crates/service-utils/src/service/types.rs | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 2b5a4edda..8bcc1d4b0 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -1,5 +1,5 @@ use diesel::{ - r2d2::{ConnectionManager, Pool}, + r2d2::{PooledConnection, ConnectionManager, Pool}, PgConnection, }; use jsonschema::JSONSchema; @@ -67,3 +67,31 @@ impl FromRequest for AuthenticationInfo { ready(result) } } + +pub struct DbConnection(pub PooledConnection>); +impl FromRequest for DbConnection { + type Error = Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let app_state = match req.app_data::>() { + Some(state) => state, + None => { + println!("Unable to get app_data from request"); + return ready(Err(error::ErrorInternalServerError(""))); + } + }; + let result = match app_state.db_pool.get() { + Ok(conn) => Ok(DbConnection(conn)), + Err(e) => { + println!("Unable to get db connection from pool, error: {e}"); + Err(error::ErrorInternalServerError("")) + } + }; + + ready(result) + } +} From 60cd4d40ffb9e773fa6be87038e8c68a7b99c85e Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Fri, 28 Jul 2023 05:20:22 +0530 Subject: [PATCH 056/352] feat:added bulk context create API --- .../src/api/context/handlers.rs | 240 +++++++++++++----- .../src/api/context/types.rs | 4 +- 2 files changed, 184 insertions(+), 60 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index e875b8dd9..1118c94a4 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -2,7 +2,10 @@ use crate::{ api::context::types::{PaginationParams, PutReq, PutResp}, db::{ models::{Context, Dimension}, - schema::cac_v1::{contexts, dimensions::dsl::dimensions}, + schema::cac_v1::{ + contexts::{self, id}, + dimensions::dsl::dimensions, + }, }, }; use actix_web::{ @@ -17,10 +20,10 @@ use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, result::{DatabaseErrorKind::*, Error::DatabaseError}, - ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, + Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; use log::info; -use serde_json::{Value, Value::Null}; +use serde_json::{json, Value, Value::Null}; use service_utils::{ helpers::ToActixErr, service::types::{AppState, AuthenticationInfo}, @@ -28,11 +31,12 @@ use service_utils::{ pub fn endpoints() -> Scope { Scope::new("") - .service(put_context) + .service(put_handler) .service(list_contexts) - .service(move_context) + .service(move_handler) .service(get_context) .service(delete_context) + .service(bulk_operations) } type DBConnection = PooledConnection>; @@ -70,7 +74,7 @@ fn val_dimensions_cal_priority( fn create_ctx_from_put_req( req: web::Json, conn: &mut DBConnection, - auth_info: AuthenticationInfo, + auth_info: &AuthenticationInfo, ) -> actix_web::Result { let ctx_condition = Value::Object(req.context.to_owned()); let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { @@ -84,7 +88,6 @@ fn create_ctx_from_put_req( }; let context_id = blake3::hash((ctx_condition).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); - let AuthenticationInfo(email) = auth_info; Ok(Context { id: context_id.clone(), @@ -93,14 +96,14 @@ fn create_ctx_from_put_req( override_id: override_id.to_owned(), override_: req.r#override.to_owned(), created_at: Utc::now(), - created_by: email, + created_by: email.to_owned(), }) } fn update_override_of_existing_ctx( conn: &mut PgConnection, ctx: Context, -) -> Result, diesel::result::Error> { +) -> Result { use contexts::dsl; let mut new_override: Value = dsl::contexts .filter(dsl::id.eq(&ctx.id)) @@ -117,7 +120,7 @@ fn update_override_of_existing_ctx( .filter(dsl::id.eq(&new_ctx.id)) .set(&new_ctx) .execute(conn)?; - Ok(web::Json(get_put_resp(new_ctx))) + Ok(get_put_resp(new_ctx)) } fn get_put_resp(ctx: Context) -> PutResp { @@ -127,88 +130,126 @@ fn get_put_resp(ctx: Context) -> PutResp { priority: ctx.priority, } } +//TO-DO : Need to create a custom error type which implements error traits +#[derive(Debug)] +enum Error { + DIESEL(diesel::result::Error), + STRTYPE(String), +} -#[put("")] -async fn put_context( +fn put( req: web::Json, - state: Data, - auth_info: AuthenticationInfo, -) -> actix_web::Result> { + auth_info: &AuthenticationInfo, + conn: &mut PooledConnection>, +) -> Result { use contexts::dsl::contexts; - let mut conn = state - .db_pool - .get() - .map_err_to_internal_server("unable to get db connection from pool", "")?; - - let new_ctx = create_ctx_from_put_req(req, &mut conn, auth_info)?; - let insert = diesel::insert_into(contexts) - .values(&new_ctx) - .execute(&mut conn); + let new_ctx = create_ctx_from_put_req(req, conn, auth_info).map_err(|e| { + log::error!("context struct creation failed with err: {e:?}"); + Error::STRTYPE(e.to_string()) + })?; + let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); match insert { - Ok(_) => Ok(web::Json(get_put_resp(new_ctx))), + Ok(_) => Ok(get_put_resp(new_ctx)), Err(DatabaseError(UniqueViolation, _)) => { - update_override_of_existing_ctx(&mut conn, new_ctx) - .map_err_to_internal_server( - "override update of existing context failed", - "", - ) + update_override_of_existing_ctx(conn, new_ctx).map_err(|e| Error::DIESEL(e)) } - e => { - println!("context insert failed with error: {e:?}"); - Err(ErrorInternalServerError("")) + Err(e) => { + log::error!("update query failed with error: {e:?}"); + Err(Error::DIESEL(e)) } } } -#[put("/move/{ctx_id}")] -async fn move_context( - state: Data, - path: Path, +#[put("")] +async fn put_handler( req: web::Json, + state: Data, auth_info: AuthenticationInfo, ) -> actix_web::Result> { - use contexts::dsl; let conn = &mut state .db_pool .get() .map_err_to_internal_server("unable to get db connection from pool", "")?; - let new_ctx = create_ctx_from_put_req(req, conn, auth_info)?; - let old_ctx_id = path.into_inner(); + put(req, &auth_info, conn) + .map(|resp| web::Json(resp)) + .map_err(|e| { + println!("context put failed with error: {:?}", e); + ErrorInternalServerError("") + }) +} + +fn r#move( + old_ctx_id: String, + req: web::Json, + auth_info: &AuthenticationInfo, + conn: &mut PooledConnection>, + with_transaction: bool, +) -> Result { + use contexts::dsl; + let new_ctx = create_ctx_from_put_req(req, conn, auth_info).map_err(|e| { + log::error!("update query failed with error: {e:?}"); + Error::STRTYPE(e.to_string()) + })?; let update = diesel::update(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - //NOTE we have to specifically set id because - //diesel's #derive(AsChangeset) by default - //assumes that we want to ignore it .set((&new_ctx, dsl::id.eq(&new_ctx.id))) .execute(conn); - let handle_unique_violation = |db_conn: &mut DBConnection, new_ctx: Context| { - db_conn - .build_transaction() - .read_write() - .run(|conn| { + let handle_unique_violation = + |db_conn: &mut DBConnection, new_ctx: Context, with_transaction: bool| { + if with_transaction { + db_conn.build_transaction().read_write().run(|conn| { + diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .execute(conn)?; + update_override_of_existing_ctx(conn, new_ctx) + }) + } else { diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - .execute(conn)?; - update_override_of_existing_ctx(conn, new_ctx) - }) - .map_err_to_internal_server("update query failed", "") - }; + .execute(db_conn)?; + update_override_of_existing_ctx(db_conn, new_ctx) + } + }; + match update { - Ok(0) => Err(ErrorNotFound(format!( + Ok(0) => Err(Error::STRTYPE(format!( "context with id: {old_ctx_id} not found" ))), - Ok(_) => Ok(web::Json(get_put_resp(new_ctx))), - Err(DatabaseError(UniqueViolation, _)) => handle_unique_violation(conn, new_ctx), + Ok(_) => Ok(get_put_resp(new_ctx)), + Err(DatabaseError(UniqueViolation, _)) => { + handle_unique_violation(conn, new_ctx, with_transaction) + .map_err(|e| Error::DIESEL(e)) + } Err(e) => { log::error!("update query failed with error: {e:?}"); - Err(ErrorInternalServerError("")) + Err(Error::DIESEL(e)) } } } +#[put("/move/{ctx_id}")] +async fn move_handler( + state: Data, + path: Path, + req: web::Json, + auth_info: AuthenticationInfo, +) -> actix_web::Result> { + let conn = &mut state + .db_pool + .get() + .map_err_to_internal_server("unable to get db connection from pool", "")?; + + r#move(path.into_inner(), req, &auth_info, conn, true) + .map(|resp| web::Json(resp)) + .map_err(|e| { + println!("move api failed with error: {:?}", e); + ErrorInternalServerError("") + }) +} + #[get("/{ctx_id}")] async fn get_context( path: web::Path, @@ -285,7 +326,6 @@ async fn delete_context( auth_info: AuthenticationInfo, ) -> actix_web::Result { use contexts::dsl; - let mut conn = state .db_pool .get() @@ -306,3 +346,87 @@ async fn delete_context( } } } + +#[derive(serde::Deserialize)] +enum ContextAction { + PUT(PutReq), + DELETE(String), + MOVE((String, PutReq)), +} + +#[put("/bulk-operations")] +async fn bulk_operations( + reqs: web::Json>, + state: Data, + auth_info: AuthenticationInfo, +) -> actix_web::Result { + use contexts::dsl::contexts; + let mut conn = state + .db_pool + .get() + .map_err_to_internal_server("Unable to get db connection from pool", "")?; + + let mut resp = Vec::>::new(); + let result = conn.transaction::<_, diesel::result::Error, _>(|transaction_conn| { + for action in reqs.into_inner().into_iter() { + match action { + ContextAction::PUT(put_req) => { + let resp_result = + put(actix_web::web::Json(put_req), &auth_info, transaction_conn); + + match resp_result { + Ok(put_resp) => { + resp.push(Ok(json!(put_resp))); + } + Err(e) => { + log::error!("Failed at insert into contexts due to {:?}", e); + return Err(diesel::result::Error::RollbackTransaction); + } + } + } + ContextAction::DELETE(ctx_id) => { + let deleted_row = + delete(contexts.filter(id.eq(&ctx_id))).execute(transaction_conn); + let AuthenticationInfo(email) = auth_info.clone(); + match deleted_row { + Ok(0) => return Err(diesel::result::Error::RollbackTransaction), + Ok(_) => { + info!("{ctx_id} context deleted by {email}"); + resp.push(Ok(json!(format!("{ctx_id} deleted succesfully")))) + } + Err(e) => { + log::error!("Delete context failed due to {:?}", e); + return Err(diesel::result::Error::RollbackTransaction); + } + }; + } + ContextAction::MOVE((old_ctx_id, put_req)) => { + let move_context_resp = r#move( + old_ctx_id, + actix_web::web::Json(put_req), + &auth_info, + transaction_conn, + false, + ); + + match move_context_resp { + Ok(move_resp) => resp.push(Ok(json!(move_resp))), + Err(e) => { + log::error!( + "Failed at moving context reponse due to {:?}", + e + ); + return Err(diesel::result::Error::RollbackTransaction); + } + }; + } + } + } + Ok(()) // Commit the transaction + }); + + match result { + Ok(_) => Ok(HttpResponse::Ok().json(resp)), // If the transaction was successful, return the responses + Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error + } +} diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index e0c81e855..03b1e101b 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -1,13 +1,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct PutReq { pub context: Map, pub r#override: Value, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct PutResp { pub context_id: String, pub override_id: String, From 9fd26345ba8222ae2fd643d543f34a43c86fcf58 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 2 Aug 2023 00:30:47 +0530 Subject: [PATCH 057/352] fix: updated response-type of /context/bulk-operations api --- crates/context-aware-config/src/api/context/handlers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 1118c94a4..9d13558b7 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -366,7 +366,7 @@ async fn bulk_operations( .get() .map_err_to_internal_server("Unable to get db connection from pool", "")?; - let mut resp = Vec::>::new(); + let mut resp = Vec::::new(); let result = conn.transaction::<_, diesel::result::Error, _>(|transaction_conn| { for action in reqs.into_inner().into_iter() { match action { @@ -376,7 +376,7 @@ async fn bulk_operations( match resp_result { Ok(put_resp) => { - resp.push(Ok(json!(put_resp))); + resp.push(json!(put_resp)); } Err(e) => { log::error!("Failed at insert into contexts due to {:?}", e); @@ -392,7 +392,7 @@ async fn bulk_operations( Ok(0) => return Err(diesel::result::Error::RollbackTransaction), Ok(_) => { info!("{ctx_id} context deleted by {email}"); - resp.push(Ok(json!(format!("{ctx_id} deleted succesfully")))) + resp.push(json!(format!("{ctx_id} deleted succesfully"))) } Err(e) => { log::error!("Delete context failed due to {:?}", e); @@ -410,7 +410,7 @@ async fn bulk_operations( ); match move_context_resp { - Ok(move_resp) => resp.push(Ok(json!(move_resp))), + Ok(move_resp) => resp.push(json!(move_resp)), Err(e) => { log::error!( "Failed at moving context reponse due to {:?}", From 412044e91424b045c5f5360f98729c7d19d5b184 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Sun, 16 Jul 2023 21:05:43 +0530 Subject: [PATCH 058/352] experimentation-platform init setup --- .env.example | 3 + Cargo.lock | 31 + crates/context-aware-config/src/main.rs | 30 +- crates/experimentation-platform/Cargo.lock | 2725 +++++++++++++++++ crates/experimentation-platform/Cargo.toml | 33 + crates/experimentation-platform/diesel.toml | 6 + .../down.sql | 6 + .../up.sql | 36 + .../down.sql | 3 + .../up.sql | 19 + .../src/api/experiments/handlers.rs | 201 ++ .../src/api/experiments/mod.rs | 2 + .../src/api/experiments/types.rs | 32 + .../experimentation-platform/src/api/mod.rs | 1 + crates/experimentation-platform/src/db/mod.rs | 2 + .../experimentation-platform/src/db/models.rs | 56 + .../experimentation-platform/src/db/schema.rs | 24 + crates/experimentation-platform/src/lib.rs | 3 + .../experimentation-platform/src/schema.patch | 19 + crates/service-utils/Cargo.toml | 2 + crates/service-utils/src/db/utils.rs | 3 +- crates/service-utils/src/service/types.rs | 11 + 22 files changed, 3245 insertions(+), 3 deletions(-) create mode 100644 crates/experimentation-platform/Cargo.lock create mode 100644 crates/experimentation-platform/Cargo.toml create mode 100644 crates/experimentation-platform/diesel.toml create mode 100644 crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql create mode 100644 crates/experimentation-platform/src/api/experiments/handlers.rs create mode 100644 crates/experimentation-platform/src/api/experiments/mod.rs create mode 100644 crates/experimentation-platform/src/api/experiments/types.rs create mode 100644 crates/experimentation-platform/src/api/mod.rs create mode 100644 crates/experimentation-platform/src/db/mod.rs create mode 100644 crates/experimentation-platform/src/db/models.rs create mode 100644 crates/experimentation-platform/src/db/schema.rs create mode 100644 crates/experimentation-platform/src/lib.rs create mode 100644 crates/experimentation-platform/src/schema.patch diff --git a/.env.example b/.env.example index 8d7db92ba..ad507bacb 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,6 @@ DB_HOST=127.0.0.1:5432 DB_NAME=config APP_ENV=DEV ADMIN_TOKEN="12345678" +ALLOW_SAME_KEYS_OVERLAPPING_CTX=false +ALLOW_DIFF_KEYS_OVERLAPPING_CTX=false +ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=false diff --git a/Cargo.lock b/Cargo.lock index 8b42c354a..9755d378d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,10 +611,12 @@ dependencies = [ "diesel-derive-enum", "dotenv", "env_logger", + "experimentation-platform", "json-patch", "jsonschema", "log", "reqwest", + "rs-snowflake", "rusoto_core", "rusoto_kms", "rusoto_signature", @@ -914,6 +916,28 @@ dependencies = [ "libc", ] +[[package]] +name = "experimentation-platform" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger", + "log", + "rs-snowflake", + "serde", + "serde_json", + "service-utils", + "strum", + "strum_macros", + "uuid 0.8.2", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1939,6 +1963,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rs-snowflake" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" + [[package]] name = "rusoto_core" version = "0.48.0" @@ -2211,6 +2241,7 @@ dependencies = [ "diesel", "dotenv", "jsonschema", + "rs-snowflake", "rusoto_core", "rusoto_kms", "rusoto_signature", diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 7c433030d..f4679e752 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -8,8 +8,14 @@ use std::{env, io::Result}; use actix_web::{ middleware::Logger, web::get, web::scope, web::Data, App, HttpResponse, HttpServer, }; +use snowflake::SnowflakeIdGenerator; +use std::sync::Mutex; -use service_utils::{db::utils::get_pool, service::types::AppState}; +use service_utils::{ + db::utils::get_pool, + service::types::{AppState, ExperimentationFlags}, + helpers::get_from_env_unsafe +}; use api::*; use helpers::get_default_config_validation_schema; @@ -20,13 +26,33 @@ async fn main() -> Result<()> { env_logger::init(); let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + let allow_same_keys_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX").expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); + let allow_diff_keys_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX").expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); + let allow_same_keys_non_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX").expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + HttpServer::new(move || { let logger: Logger = Logger::default(); App::new() .app_data(Data::new(AppState { db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), - admin_token: admin_token.to_owned(), + admin_token: admin_token.to_owned(), + + experimentation_flags: ExperimentationFlags { + allow_same_keys_overlapping_ctx: + allow_same_keys_overlapping_ctx.to_owned(), + allow_diff_keys_overlapping_ctx: + allow_diff_keys_overlapping_ctx.to_owned(), + allow_same_keys_non_overlapping_ctx: + allow_same_keys_non_overlapping_ctx.to_owned(), + }, + + snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1, 1)), })) .wrap(logger) .route( diff --git a/crates/experimentation-platform/Cargo.lock b/crates/experimentation-platform/Cargo.lock new file mode 100644 index 000000000..d3a5f0a06 --- /dev/null +++ b/crates/experimentation-platform/Cargo.lock @@ -0,0 +1,2725 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags 1.3.2", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.2", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.23", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bb1b4028935821b2d6b439bba2e970bdcf740832732437ead910c632e30d7d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae467cbb0111869b765e13882a1dbbd6cb52f58203d8b80c44f667d4dd19843" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.23", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diesel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" +dependencies = [ + "bitflags 2.3.3", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", + "serde_json", + "uuid 1.4.1", +] + +[[package]] +name = "diesel-derive-enum" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "diesel_derives" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.26", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "experimentation-platform" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger", + "log", + "rs-snowflake", + "serde", + "serde_json", + "service-utils", + "strum", + "strum_macros", + "uuid 0.8.2", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.4", + "windows-sys", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash 0.8.3", + "anyhow", + "base64 0.21.2", + "bytecount", + "clap", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time 0.3.23", + "url", + "uuid 1.4.1", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rs-snowflake" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" + +[[package]] +name = "rusoto_core" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_kms" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "serde", + "serde_json", +] + +[[package]] +name = "rusoto_signature" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" +dependencies = [ + "base64 0.13.1", + "bytes", + "chrono", + "digest 0.9.0", + "futures", + "hex", + "hmac", + "http", + "hyper", + "log", + "md-5", + "percent-encoding", + "pin-project-lite", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "service-utils" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "base64 0.21.2", + "bytes", + "diesel", + "dotenv", + "jsonschema", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "urlencoding", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix 0.37.23", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.26", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xml-rs" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml new file mode 100644 index 000000000..6ee35fecf --- /dev/null +++ b/crates/experimentation-platform/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "experimentation-platform" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = "0.15.0" +# Https server framework +actix = "0.13.0" +actix-web = "4.0.0" +# To help generate snowflake ids +rs-snowflake = "0.6.0" +# To help with generating uuids +uuid = {version = "^0.8", features = ["v4","serde"]} +# To serialize and deserialize objects from json +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +# For logging and debugging +env_logger = "0.8" +log = "^0.4" +# to work with enums +strum_macros = "^0.24" +strum = {version = "^0.24"} +derive_more = "^0.99" +# date and time +chrono = { version = "0.4", features = ["serde"] } +# ORM +diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +service-utils = { path = "../service-utils" } diff --git a/crates/experimentation-platform/diesel.toml b/crates/experimentation-platform/diesel.toml new file mode 100644 index 000000000..da017c980 --- /dev/null +++ b/crates/experimentation-platform/diesel.toml @@ -0,0 +1,6 @@ +[print_schema] +file = "src/db/schema.rs" +patch_file = "src/schema.patch" + +[migrations_directory] +dir = "migrations" diff --git a/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 000000000..a9f526091 --- /dev/null +++ b/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 000000000..d68895b1a --- /dev/null +++ b/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql new file mode 100644 index 000000000..cde08addb --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TABLE experiments; +DROP TYPE experiment_status_type; diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql new file mode 100644 index 000000000..8416c896b --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here +CREATE TYPE experiment_status_type as enum ('CREATED', 'CONCLUDED', 'INPROGRESS'); + +CREATE TABLE experiments ( + id BIGINT PRIMARY KEY, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by TEXT NOT NULL, + + name TEXT NOT NULL, + override_keys TEXT[] NOT NULL, + status experiment_status_type NOT NULL, + traffic_percentage INTEGER NOT NULL CHECK (traffic_percentage >= 0), + + context JSON NOT NULL, + variants JSON NOT NULL, + + constraint check_override_keys_not_null + check ( array_position(override_keys, null) is null and override_keys <> '{}' ) +) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs new file mode 100644 index 000000000..92d1bdbe6 --- /dev/null +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -0,0 +1,201 @@ +use actix_web::{ + post, + web::{self, Data}, + HttpResponse, + Scope, +}; +use chrono::Utc; +use serde_json::Value; +use diesel::pg::PgConnection; +use diesel::{QueryResult, RunQueryDsl, sql_query}; +use std::collections::{HashSet, HashMap}; + +use service_utils::service::types::AppState; + +use super::types::ExperimentCreateReq; +use crate::db::models::{Experiment, ExperimentStatusType}; + +pub fn endpoints() -> Scope { + Scope::new("/experiments") + .service(create) +} + +fn get_active_experiments(conn: &mut PgConnection) -> QueryResult> { + let active_experiments = sql_query( + "SELECT * FROM experiments WHERE status = 'CREATED' OR status = 'INPROGRESS'" + ); + return active_experiments.load(conn); +} + +fn are_overlapping_contexts(context_a_json: &Value, context_b_json: &Value) -> bool { + // TODO: pattern match conversion to object instead of throwing err + let context_a = context_a_json.as_object().expect("contextA not an object"); + let context_b = context_b_json.as_object().expect("contextB not an object"); + + let context_a_keys = context_a.keys(); + let context_b_keys = context_b.keys(); + + let ref_keys = + if context_a_keys.len() > context_b_keys.len() { + context_b_keys + } else { + context_a_keys + }; + + let mut is_overlapping = true; + for key in ref_keys { + let test = + (context_a.contains_key(key) && context_b.contains_key(key)) + && (context_a[key] == context_b[key]); + is_overlapping = is_overlapping && test; + + if !test { + break; + } + } + + is_overlapping +} + +pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Value { + match json { + Value::Object(m) => { + let mut m = m.clone(); + for (k, v) in fields { + m.insert(k.clone(), Value::String(v.clone())); + } + Value::Object(m) + }, + + v => v.clone(), + } +} + +#[post("")] +async fn create(state: Data, req: web::Json) -> HttpResponse { + use crate::db::schema::experiments::dsl::experiments; + + let override_keys = &req.override_keys; + let variants = req.variants.to_vec(); + + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, + Err(e) => { + println!("unable to get db connection from pool, error: {e}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + // Checking if all the variants are overriding the mentioned keys + for variant in &variants { + let overrides = &variant.overrides; + let mut is_valid_variant = false; + + for override_key in override_keys { + let has_override_key = + match overrides[override_key] { + Value::Null => false, + _ => true, + }; + is_valid_variant = is_valid_variant && has_override_key; + } + + if !is_valid_variant { + return HttpResponse::BadRequest() + .json( + "{\"message\" : \"all variants should contain the keys mentioned override_keys\"}" + ); + } + } + + //traffic_percentage should be max 100/length of variants + //read the envs related to falgs to check the overlapping + let flags = &state.experimentation_flags; + + //check for overlapping context experiments + if !req.context.is_object() { + return HttpResponse::BadRequest() + .json( + "{\"message\": \"context should be a map of key value pairs\" }" + ) + } + + // let context_obj = req.context.as_object().expect("context is not a map"); + let active_experiments: Vec = + get_active_experiments(&mut conn).expect("Failed to get active experiments!"); + + let mut valid_experiment = true; + if + !flags.allow_same_keys_overlapping_ctx + || !flags.allow_diff_keys_overlapping_ctx + || !flags.allow_same_keys_non_overlapping_ctx + { + let override_keys_set: HashSet<_> = override_keys.iter().collect(); + for active_experiment in active_experiments.iter() { + let are_overlapping = + are_overlapping_contexts(&req.context, &active_experiment.context); + + let have_intersecting_key_set = + active_experiment.override_keys.iter().any(|key| override_keys_set.contains(key)); + + if !flags.allow_diff_keys_overlapping_ctx { + valid_experiment = valid_experiment && !are_overlapping; + } + if !flags.allow_same_keys_overlapping_ctx { + valid_experiment = valid_experiment && (are_overlapping && !have_intersecting_key_set); + } + if !flags.allow_same_keys_non_overlapping_ctx { + valid_experiment = valid_experiment && (!are_overlapping && !have_intersecting_key_set); + } + } + } + + if !valid_experiment { + return HttpResponse::BadRequest() + .json("invalid experiment config"); + } + + // create id for experiment + let mut snowflake_generator = state.snowflake_generator.lock().unwrap(); + let experiment_id = snowflake_generator.real_time_generate(); + + //create overrides in CAC, if successfull then create experiment in DB + + for mut variant in variants { + let variant_id = experiment_id.to_string() + &variant.id; + let fields_to_add = HashMap::from([ ("variant".to_string(), variant_id.to_string()) ]); + + let updated_context = add_fields_to_json(&req.context, &fields_to_add); + // call cac to send updated_context and req.overrides + + // update variant.id to => experiment_id + variant.id + variant.id = variant_id; + } + + let new_experiment = Experiment { + id: experiment_id, + created_by: "NA".to_string(), + created_at: Utc::now(), + + name: req.name.to_string(), + override_keys: req.override_keys.to_vec(), + traffic_percentage: req.traffic_percentage, + + status: ExperimentStatusType::CREATED, + + context: req.context.clone(), + variants: serde_json::to_value(&req.variants).unwrap(), + }; + + let insert = diesel::insert_into(experiments) + .values(&new_experiment) + .execute(&mut conn); + + match insert { + Ok(_) => return HttpResponse::Created().json(new_experiment), + Err(e) => { + println!("Experiment creation failed with error: {e}"); + return HttpResponse::InternalServerError().body("Failed to create experiment\n"); + } + } +} diff --git a/crates/experimentation-platform/src/api/experiments/mod.rs b/crates/experimentation-platform/src/api/experiments/mod.rs new file mode 100644 index 000000000..4d863a520 --- /dev/null +++ b/crates/experimentation-platform/src/api/experiments/mod.rs @@ -0,0 +1,2 @@ +pub mod handlers; +pub mod types; diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs new file mode 100644 index 000000000..fa870893a --- /dev/null +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use crate::db::models; + +#[derive(Deserialize, Serialize, Clone)] +pub enum VariantType { + CONTROL, + EXPERIMENTAL +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub overrides: Value +} + +#[derive(Deserialize)] +pub struct ExperimentCreateReq { + pub name: String, + pub override_keys: Vec, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Vec, +} + +#[derive(Serialize)] +pub struct ExperimentCreateRes { + pub message: String, + pub data: models::Experiment, +} diff --git a/crates/experimentation-platform/src/api/mod.rs b/crates/experimentation-platform/src/api/mod.rs new file mode 100644 index 000000000..c5b8fbb5b --- /dev/null +++ b/crates/experimentation-platform/src/api/mod.rs @@ -0,0 +1 @@ +pub mod experiments; diff --git a/crates/experimentation-platform/src/db/mod.rs b/crates/experimentation-platform/src/db/mod.rs new file mode 100644 index 000000000..d5cbad7e2 --- /dev/null +++ b/crates/experimentation-platform/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod schema; diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs new file mode 100644 index 000000000..9d5624ed7 --- /dev/null +++ b/crates/experimentation-platform/src/db/models.rs @@ -0,0 +1,56 @@ +use crate::db::schema::*; +use chrono::offset::Utc; +use chrono::DateTime; +use diesel::{Selectable, Queryable, Insertable, QueryableByName}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, diesel_derive_enum::DbEnum)] +#[DbValueStyle = "UPPERCASE"] +#[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] +pub enum ExperimentStatusType { + CREATED, + CONCLUDED, + INPROGRESS, +} + +/*** +impl ToSql for ExperimentStatusType { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { + match *self { + ExperimentStatusType::CREATED => out.write_all("CREATED"), + ExperimentStatusType::CONCLUDED => out.write_all("CONCLUDED"), + ExperimentStatusType::INPROGRESS => out.write_all("INPROGRESS"), + } + Ok(IsNull::No) + } +} + +impl FromSql for ExperimentStatusType { + fn from_sql(bytes: PgValue<'_>)-> deserialize::Result { + match bytes.as_bytes() { + b"CREATED" => Ok(ExperimentStatusType::CREATED), + b"CONCLUDED" => Ok(ExperimentStatusType::CONCLUDED), + b"INPROGRESS" => Ok(ExperimentStatusType::INPROGRESS), + _ => Err("Unrecognized enum variant".into()), + } + } +} +**/ + +#[derive(QueryableByName, Queryable, Selectable, Insertable, Serialize, Clone)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(id))] +pub struct Experiment { + pub id: i64, + pub created_by: String, + pub created_at: DateTime, + + pub name: String, + pub override_keys: Vec, + pub traffic_percentage: i32, + pub status: ExperimentStatusType, + + pub context: Value, + pub variants: Value +} diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs new file mode 100644 index 000000000..3d4f52bfa --- /dev/null +++ b/crates/experimentation-platform/src/db/schema.rs @@ -0,0 +1,24 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "experiment_status_type"))] + pub struct ExperimentStatusType; +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ExperimentStatusType; + + experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + name -> Text, + override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + } +} diff --git a/crates/experimentation-platform/src/lib.rs b/crates/experimentation-platform/src/lib.rs new file mode 100644 index 000000000..0664c941c --- /dev/null +++ b/crates/experimentation-platform/src/lib.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod db; + diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch new file mode 100644 index 000000000..72c02779a --- /dev/null +++ b/crates/experimentation-platform/src/schema.patch @@ -0,0 +1,19 @@ +diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs +index 670548b..3d4f52b 100644 +--- a/crates/experimentation-platform/src/db/schema.rs ++++ b/crates/experimentation-platform/src/db/schema.rs +@@ -12,13 +12,13 @@ diesel::table! { + + experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + name -> Text, +- override_keys -> Array>, ++ override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + } + } diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 6c3efcc77..b0b00bb33 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -11,6 +11,8 @@ dotenv = "0.15.0" # Https server framework actix = "0.13.0" actix-web = "4.0.0" +# To help generate snowflake ids +rs-snowflake = "0.6.0" #ORM diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } rusoto_kms = "0.48.0" diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index 6d7426b0d..05d9a00b3 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,3 +1,4 @@ + use crate::aws::kms; use crate::helpers::get_from_env_unsafe; use diesel::{ @@ -23,4 +24,4 @@ pub async fn get_pool() -> Pool> { Pool::builder() .build(manager) .expect("Error building a connection pool") -} +} \ No newline at end of file diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 8bcc1d4b0..6b08a6a5d 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -11,10 +11,21 @@ use std::{ use actix_web::{error, web::Data, Error, FromRequest}; +use snowflake::SnowflakeIdGenerator; +use std::sync::Mutex; + +pub struct ExperimentationFlags { + pub allow_same_keys_overlapping_ctx : bool, + pub allow_diff_keys_overlapping_ctx : bool, + pub allow_same_keys_non_overlapping_ctx: bool, +} + pub struct AppState { pub db_pool: Pool>, pub default_config_validation_schema: JSONSchema, pub admin_token: String, + pub experimentation_flags: ExperimentationFlags, + pub snowflake_generator: Mutex, } #[derive(Clone)] From bd20bdf0c83d6efe52a44453a6c43c5374f9e83f Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Mon, 24 Jul 2023 10:18:53 +0530 Subject: [PATCH 059/352] feat: added list experiments API --- .editorconfig | 19 + .gitignore | 3 +- .vscode/settings.json | 3 - crates/experimentation-platform/Cargo.lock | 2725 ----------------- .../src/api/errors.rs | 33 + .../src/api/experiments/handlers.rs | 136 +- .../src/api/experiments/queries.rs | 0 .../src/api/experiments/types.rs | 16 +- .../experimentation-platform/src/api/mod.rs | 1 + .../experimentation-platform/src/db/models.rs | 12 +- crates/service-utils/src/helpers.rs | 3 + flake.nix | 1 + 12 files changed, 174 insertions(+), 2778 deletions(-) create mode 100644 .editorconfig delete mode 100644 .vscode/settings.json delete mode 100644 crates/experimentation-platform/Cargo.lock create mode 100644 crates/experimentation-platform/src/api/errors.rs create mode 100644 crates/experimentation-platform/src/api/experiments/queries.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..34811a401 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +[makefile] + +charset = utf-8 +indent_style = tab +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 565e77763..3dfc43b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,5 @@ bacon.toml docker-compose/localstack/export_cyphers.sh test_logs -.keep \ No newline at end of file +.keep +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6a99eb2da..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files.trimTrailingWhitespace": true -} \ No newline at end of file diff --git a/crates/experimentation-platform/Cargo.lock b/crates/experimentation-platform/Cargo.lock deleted file mode 100644 index d3a5f0a06..000000000 --- a/crates/experimentation-platform/Cargo.lock +++ /dev/null @@ -1,2725 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" -dependencies = [ - "actix-rt", - "actix_derive", - "bitflags 1.3.2", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" -dependencies = [ - "bitflags 1.3.2", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash 0.8.3", - "base64 0.21.2", - "bitflags 1.3.2", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash 0.7.6", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time 0.3.23", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "actix_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "addr2line" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "serde", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" - -[[package]] -name = "anstyle-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" - -[[package]] -name = "async-trait" -version = "0.1.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "bytecount" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "bytestring" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74bb1b4028935821b2d6b439bba2e970bdcf740832732437ead910c632e30d7d" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae467cbb0111869b765e13882a1dbbd6cb52f58203d8b80c44f667d4dd19843" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time 0.3.23", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "diesel" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" -dependencies = [ - "bitflags 2.3.3", - "byteorder", - "chrono", - "diesel_derives", - "itoa", - "pq-sys", - "r2d2", - "serde_json", - "uuid 1.4.1", -] - -[[package]] -name = "diesel-derive-enum" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "diesel_derives" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.26", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "experimentation-platform" -version = "0.1.0" -dependencies = [ - "actix", - "actix-web", - "chrono", - "derive_more", - "diesel", - "diesel-derive-enum", - "dotenv", - "env_logger", - "log", - "rs-snowflake", - "serde", - "serde_json", - "service-utils", - "strum", - "strum_macros", - "uuid 0.8.2", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set", - "regex", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" - -[[package]] -name = "h2" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.2", - "libc", - "windows-sys", -] - -[[package]] -name = "ipnet" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.4", - "windows-sys", -] - -[[package]] -name = "iso8601" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" -dependencies = [ - "nom", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonschema" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" -dependencies = [ - "ahash 0.8.3", - "anyhow", - "base64 0.21.2", - "bytecount", - "clap", - "fancy-regex", - "fraction", - "getrandom", - "iso8601", - "itoa", - "memchr", - "num-cmp", - "once_cell", - "parking_lot", - "percent-encoding", - "regex", - "reqwest", - "serde", - "serde_json", - "time 0.3.23", - "url", - "uuid 1.4.1", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" - -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "md-5" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[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-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.2", - "libc", -] - -[[package]] -name = "object" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "openssl" -version = "0.10.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pin-project-lite" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "pq-sys" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" -dependencies = [ - "vcpkg", -] - -[[package]] -name = "proc-macro2" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rs-snowflake" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" - -[[package]] -name = "rusoto_core" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" -dependencies = [ - "async-trait", - "base64 0.13.1", - "bytes", - "crc32fast", - "futures", - "http", - "hyper", - "hyper-tls", - "lazy_static", - "log", - "rusoto_credential", - "rusoto_signature", - "rustc_version", - "serde", - "serde_json", - "tokio", - "xml-rs", -] - -[[package]] -name = "rusoto_credential" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" -dependencies = [ - "async-trait", - "chrono", - "dirs-next", - "futures", - "hyper", - "serde", - "serde_json", - "shlex", - "tokio", - "zeroize", -] - -[[package]] -name = "rusoto_kms" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" -dependencies = [ - "async-trait", - "bytes", - "futures", - "rusoto_core", - "serde", - "serde_json", -] - -[[package]] -name = "rusoto_signature" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" -dependencies = [ - "base64 0.13.1", - "bytes", - "chrono", - "digest 0.9.0", - "futures", - "hex", - "hmac", - "http", - "hyper", - "log", - "md-5", - "percent-encoding", - "pin-project-lite", - "rusoto_credential", - "rustc_version", - "serde", - "sha2", - "tokio", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys", -] - -[[package]] -name = "rustix" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" -dependencies = [ - "bitflags 2.3.3", - "errno", - "libc", - "linux-raw-sys 0.4.3", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" - -[[package]] -name = "serde" -version = "1.0.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "serde_json" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "service-utils" -version = "0.1.0" -dependencies = [ - "actix", - "actix-web", - "base64 0.21.2", - "bytes", - "diesel", - "dotenv", - "jsonschema", - "rs-snowflake", - "rusoto_core", - "rusoto_kms", - "rusoto_signature", - "urlencoding", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" -dependencies = [ - "autocfg", - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix 0.37.23", - "windows-sys", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" -dependencies = [ - "autocfg", - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom", - "serde", -] - -[[package]] -name = "uuid" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.26", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.26", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "xml-rs" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" - -[[package]] -name = "zeroize" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" - -[[package]] -name = "zstd" -version = "0.12.3+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/crates/experimentation-platform/src/api/errors.rs b/crates/experimentation-platform/src/api/errors.rs new file mode 100644 index 000000000..1b8688bc9 --- /dev/null +++ b/crates/experimentation-platform/src/api/errors.rs @@ -0,0 +1,33 @@ +use actix_web::{ + error, + http::{header::ContentType, StatusCode}, + web::Json, + HttpResponse, +}; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Error, Display)] +#[display(fmt = "experiment error: {}", message)] +pub struct AppError { + pub message: String, + pub possible_fix: String, + pub status_code: StatusCode, +} + +#[derive(Deserialize, Serialize)] +struct ErrorResponse { + message: String, + possible_fix: String, +} + +impl error::ResponseError for AppError { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code) + .insert_header(ContentType::json()) + .json(Json(ErrorResponse { + message: self.message.to_string(), + possible_fix: self.possible_fix.to_string(), + })) + } +} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 92d1bdbe6..f41d68404 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -1,29 +1,32 @@ use actix_web::{ + get, + http::StatusCode, post, - web::{self, Data}, - HttpResponse, - Scope, + web::{self, Data, Json, Query}, + HttpResponse, Scope, }; use chrono::Utc; -use serde_json::Value; use diesel::pg::PgConnection; -use diesel::{QueryResult, RunQueryDsl, sql_query}; -use std::collections::{HashSet, HashMap}; +use diesel::{sql_query, QueryResult, RunQueryDsl}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; use service_utils::service::types::AppState; use super::types::ExperimentCreateReq; -use crate::db::models::{Experiment, ExperimentStatusType}; +use crate::{ + api::{errors::AppError, experiments::types::ListFilters}, + db::models::{Experiment, ExperimentStatusType, Experiments}, +}; pub fn endpoints() -> Scope { - Scope::new("/experiments") - .service(create) + Scope::new("/experiments").service(create) } fn get_active_experiments(conn: &mut PgConnection) -> QueryResult> { let active_experiments = sql_query( - "SELECT * FROM experiments WHERE status = 'CREATED' OR status = 'INPROGRESS'" - ); + "SELECT * FROM experiments WHERE status = 'CREATED' OR status = 'INPROGRESS'", + ); return active_experiments.load(conn); } @@ -35,17 +38,15 @@ fn are_overlapping_contexts(context_a_json: &Value, context_b_json: &Value) -> b let context_a_keys = context_a.keys(); let context_b_keys = context_b.keys(); - let ref_keys = - if context_a_keys.len() > context_b_keys.len() { - context_b_keys - } else { - context_a_keys - }; + let ref_keys = if context_a_keys.len() > context_b_keys.len() { + context_b_keys + } else { + context_a_keys + }; let mut is_overlapping = true; for key in ref_keys { - let test = - (context_a.contains_key(key) && context_b.contains_key(key)) + let test = (context_a.contains_key(key) && context_b.contains_key(key)) && (context_a[key] == context_b[key]); is_overlapping = is_overlapping && test; @@ -54,7 +55,7 @@ fn are_overlapping_contexts(context_a_json: &Value, context_b_json: &Value) -> b } } - is_overlapping + is_overlapping } pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Value { @@ -65,14 +66,17 @@ pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Val m.insert(k.clone(), Value::String(v.clone())); } Value::Object(m) - }, + } v => v.clone(), } } #[post("")] -async fn create(state: Data, req: web::Json) -> HttpResponse { +async fn create( + state: Data, + req: web::Json, +) -> HttpResponse { use crate::db::schema::experiments::dsl::experiments; let override_keys = &req.override_keys; @@ -92,11 +96,10 @@ async fn create(state: Data, req: web::Json) -> H let mut is_valid_variant = false; for override_key in override_keys { - let has_override_key = - match overrides[override_key] { - Value::Null => false, - _ => true, - }; + let has_override_key = match overrides[override_key] { + Value::Null => false, + _ => true, + }; is_valid_variant = is_valid_variant && has_override_key; } @@ -115,44 +118,44 @@ async fn create(state: Data, req: web::Json) -> H //check for overlapping context experiments if !req.context.is_object() { return HttpResponse::BadRequest() - .json( - "{\"message\": \"context should be a map of key value pairs\" }" - ) + .json("{\"message\": \"context should be a map of key value pairs\" }"); } // let context_obj = req.context.as_object().expect("context is not a map"); let active_experiments: Vec = - get_active_experiments(&mut conn).expect("Failed to get active experiments!"); + get_active_experiments(&mut conn).expect("Failed to get active experiments!"); let mut valid_experiment = true; - if - !flags.allow_same_keys_overlapping_ctx + if !flags.allow_same_keys_overlapping_ctx || !flags.allow_diff_keys_overlapping_ctx || !flags.allow_same_keys_non_overlapping_ctx { - let override_keys_set: HashSet<_> = override_keys.iter().collect(); + let override_keys_set: HashSet<_> = req.override_keys.iter().collect(); for active_experiment in active_experiments.iter() { let are_overlapping = are_overlapping_contexts(&req.context, &active_experiment.context); - let have_intersecting_key_set = - active_experiment.override_keys.iter().any(|key| override_keys_set.contains(key)); + let have_intersecting_key_set = active_experiment + .override_keys + .iter() + .any(|key| override_keys_set.contains(key)); if !flags.allow_diff_keys_overlapping_ctx { valid_experiment = valid_experiment && !are_overlapping; } if !flags.allow_same_keys_overlapping_ctx { - valid_experiment = valid_experiment && (are_overlapping && !have_intersecting_key_set); + valid_experiment = + valid_experiment && (are_overlapping && !have_intersecting_key_set); } if !flags.allow_same_keys_non_overlapping_ctx { - valid_experiment = valid_experiment && (!are_overlapping && !have_intersecting_key_set); + valid_experiment = + valid_experiment && (!are_overlapping && !have_intersecting_key_set); } } } if !valid_experiment { - return HttpResponse::BadRequest() - .json("invalid experiment config"); + return HttpResponse::BadRequest().json("invalid experiment config"); } // create id for experiment @@ -163,7 +166,8 @@ async fn create(state: Data, req: web::Json) -> H for mut variant in variants { let variant_id = experiment_id.to_string() + &variant.id; - let fields_to_add = HashMap::from([ ("variant".to_string(), variant_id.to_string()) ]); + let fields_to_add = + HashMap::from([("variant".to_string(), variant_id.to_string())]); let updated_context = add_fields_to_json(&req.context, &fields_to_add); // call cac to send updated_context and req.overrides @@ -195,7 +199,55 @@ async fn create(state: Data, req: web::Json) -> H Ok(_) => return HttpResponse::Created().json(new_experiment), Err(e) => { println!("Experiment creation failed with error: {e}"); - return HttpResponse::InternalServerError().body("Failed to create experiment\n"); + return HttpResponse::InternalServerError() + .body("Failed to create experiment\n"); } } } + +#[get("/list")] +async fn list_experiments( + state: Data, + filters: Query, +) -> actix_web::Result, AppError> { + let conn = match state.db_pool.get() { + Ok(conn) => &mut conn, + Err(e) => { + println!("Unable to get db connection from pool, error: {e}"); + return Err(AppError { + message: "Could not connect to the database".to_string(), + possible_fix: "Try after sometime".to_string(), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + }); + // return an error + } + }; + + let db_result = exp + .filter(status.eq_any(filters.status)) + .filter(created_at.ge(filters.from_date)) + .filter(created_at.le(filters.to_date)) + .limit(filters.count) + .offset((filters.page - 1) * filters.count) + .load(conn); + + let response = match db_result { + Ok(result) => Json(result), + Err(e) => { + return Err(match e { + NotFound => AppError { + message: String::from("No results found"), + possible_fix: String::from("Update your filter parameters"), + status_code: StatusCode::NOT_FOUND, + }, + DatabaseError => AppError { + message: String::from("Something went wrong"), + possible_fix: String::from("Please try again later"), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + }, + }) + } + }; + + return Ok(response); +} diff --git a/crates/experimentation-platform/src/api/experiments/queries.rs b/crates/experimentation-platform/src/api/experiments/queries.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index fa870893a..99b8f9a70 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,18 +1,21 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::db::models; +use crate::db::models::ExperimentStatusType; + #[derive(Deserialize, Serialize, Clone)] pub enum VariantType { CONTROL, - EXPERIMENTAL + EXPERIMENTAL, } #[derive(Deserialize, Serialize, Clone)] pub struct Variant { pub id: String, pub variant_type: VariantType, - pub overrides: Value + pub overrides: Value, } #[derive(Deserialize)] @@ -30,3 +33,12 @@ pub struct ExperimentCreateRes { pub message: String, pub data: models::Experiment, } + +#[derive(Deserialize, Debug)] +pub struct ListFilters { + pub status: Vec, + pub from_date: DateTime, + pub to_date: DateTime, + pub page: i64, + pub count: i64, +} diff --git a/crates/experimentation-platform/src/api/mod.rs b/crates/experimentation-platform/src/api/mod.rs index c5b8fbb5b..8cfa73a69 100644 --- a/crates/experimentation-platform/src/api/mod.rs +++ b/crates/experimentation-platform/src/api/mod.rs @@ -1 +1,2 @@ pub mod experiments; +pub mod errors; \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index 9d5624ed7..be6d8a6f7 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -1,7 +1,7 @@ use crate::db::schema::*; use chrono::offset::Utc; use chrono::DateTime; -use diesel::{Selectable, Queryable, Insertable, QueryableByName}; +use diesel::{Insertable, Queryable, QueryableByName, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -9,9 +9,9 @@ use serde_json::Value; #[DbValueStyle = "UPPERCASE"] #[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] pub enum ExperimentStatusType { - CREATED, - CONCLUDED, - INPROGRESS, + CREATED, + CONCLUDED, + INPROGRESS, } /*** @@ -28,7 +28,7 @@ impl ToSql for Experimen impl FromSql for ExperimentStatusType { fn from_sql(bytes: PgValue<'_>)-> deserialize::Result { - match bytes.as_bytes() { + match bytes.as_bytes() { b"CREATED" => Ok(ExperimentStatusType::CREATED), b"CONCLUDED" => Ok(ExperimentStatusType::CONCLUDED), b"INPROGRESS" => Ok(ExperimentStatusType::INPROGRESS), @@ -54,3 +54,5 @@ pub struct Experiment { pub context: Value, pub variants: Value } + +pub type Experiments = Vec; diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index f1778f376..a113fd8f7 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -45,3 +45,6 @@ where }) } } + + +// pub fn db_connection_from_state(state: T) -> diff --git a/flake.nix b/flake.nix index 6122364ae..1d0b19e16 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ # For `nix develop`: devShell = pkgs.mkShell { + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; nativeBuildInputs = let univPkgs = with pkgs; [ From 91c2466735040e948e16c65b5633e46b478ffd6c Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 24 Jul 2023 19:45:34 +0530 Subject: [PATCH 060/352] refactor: improvements to APIs --- .env.example | 6 +- crates/context-aware-config/Cargo.toml | 3 + crates/context-aware-config/src/main.rs | 29 +- .../up.sql | 2 +- .../src/api/experiments/handlers.rs | 257 ++++++++---------- .../src/api/experiments/helpers.rs | 165 +++++++++++ .../src/api/experiments/mod.rs | 2 + .../src/api/experiments/types.rs | 49 +++- .../experimentation-platform/src/api/mod.rs | 2 +- .../experimentation-platform/src/db/models.rs | 48 +--- crates/experimentation-platform/src/lib.rs | 1 - 11 files changed, 362 insertions(+), 202 deletions(-) create mode 100644 crates/experimentation-platform/src/api/experiments/helpers.rs diff --git a/.env.example b/.env.example index ad507bacb..6dea774b1 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,6 @@ DB_HOST=127.0.0.1:5432 DB_NAME=config APP_ENV=DEV ADMIN_TOKEN="12345678" -ALLOW_SAME_KEYS_OVERLAPPING_CTX=false -ALLOW_DIFF_KEYS_OVERLAPPING_CTX=false -ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=false +ALLOW_SAME_KEYS_OVERLAPPING_CTX=true +ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true +ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 931f69d63..62b154b44 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -11,6 +11,8 @@ dotenv = "0.15.0" # Https server framework actix = "0.13.0" actix-web = "4.0.0" +# To help generate snowflake ids +rs-snowflake = "0.6.0" # To help with generating uuids uuid = {version = "^0.8", features = ["v4","serde"]} # To serialize and deserialize objects from json @@ -40,3 +42,4 @@ reqwest = { version = "*", features = [ "rustls-tls" ] } json-patch = "1.0.0" service-utils = { path = "../service-utils" } +experimentation-platform = { path = "../experimentation-platform"} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index f4679e752..a9f6c1cac 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -13,13 +13,15 @@ use std::sync::Mutex; use service_utils::{ db::utils::get_pool, - service::types::{AppState, ExperimentationFlags}, - helpers::get_from_env_unsafe + helpers::get_from_env_unsafe, + service::types::{AppState, ExperimentationFlags} }; use api::*; use helpers::get_default_config_validation_schema; +use experimentation_platform::api::*; + #[actix_web::main] async fn main() -> Result<()> { dotenv::dotenv().ok(); @@ -29,9 +31,15 @@ async fn main() -> Result<()> { /****** EXPERIMENTATION PLATFORM ENVs *********/ - let allow_same_keys_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX").expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); - let allow_diff_keys_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX").expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); - let allow_same_keys_non_overlapping_ctx: bool = get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX").expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); + let allow_same_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); + let allow_diff_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); + let allow_same_keys_non_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); /****** EXPERIMENTATION PLATFORM ENVs *********/ @@ -41,13 +49,13 @@ async fn main() -> Result<()> { .app_data(Data::new(AppState { db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), - admin_token: admin_token.to_owned(), + admin_token: admin_token.to_owned(), experimentation_flags: ExperimentationFlags { - allow_same_keys_overlapping_ctx: - allow_same_keys_overlapping_ctx.to_owned(), - allow_diff_keys_overlapping_ctx: - allow_diff_keys_overlapping_ctx.to_owned(), + allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx + .to_owned(), + allow_diff_keys_overlapping_ctx: allow_diff_keys_overlapping_ctx + .to_owned(), allow_same_keys_non_overlapping_ctx: allow_same_keys_non_overlapping_ctx.to_owned(), }, @@ -64,6 +72,7 @@ async fn main() -> Result<()> { .service(scope("/dimension").service(dimension::endpoints())) .service(scope("/default-config").service(default_config::endpoints())) .service(scope("/config").service(config::endpoints())) + .service(experiments::endpoints()) }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql index 8416c896b..20f1d9183 100644 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql @@ -14,6 +14,6 @@ CREATE TABLE experiments ( context JSON NOT NULL, variants JSON NOT NULL, - constraint check_override_keys_not_null + constraint check_override_keys_not_null check ( array_position(override_keys, null) is null and override_keys <> '{}' ) ) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index f41d68404..e27728000 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -3,204 +3,165 @@ use actix_web::{ http::StatusCode, post, web::{self, Data, Json, Query}, - HttpResponse, Scope, + Scope, }; use chrono::Utc; -use diesel::pg::PgConnection; -use diesel::{sql_query, QueryResult, RunQueryDsl}; -use serde_json::Value; -use std::collections::{HashMap, HashSet}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use std::collections::HashMap; -use service_utils::service::types::AppState; +use service_utils::service::types::{AppState, AuthenticationInfo}; -use super::types::ExperimentCreateReq; +use super::{ + helpers::{ + add_fields_to_json, check_variant_types, check_variants_override_coverage, + validate_experiment, + }, + types::{ExperimentCreateRequest, ExperimentCreateResponse}, +}; use crate::{ api::{errors::AppError, experiments::types::ListFilters}, db::models::{Experiment, ExperimentStatusType, Experiments}, }; pub fn endpoints() -> Scope { - Scope::new("/experiments").service(create) -} - -fn get_active_experiments(conn: &mut PgConnection) -> QueryResult> { - let active_experiments = sql_query( - "SELECT * FROM experiments WHERE status = 'CREATED' OR status = 'INPROGRESS'", - ); - return active_experiments.load(conn); -} - -fn are_overlapping_contexts(context_a_json: &Value, context_b_json: &Value) -> bool { - // TODO: pattern match conversion to object instead of throwing err - let context_a = context_a_json.as_object().expect("contextA not an object"); - let context_b = context_b_json.as_object().expect("contextB not an object"); - - let context_a_keys = context_a.keys(); - let context_b_keys = context_b.keys(); - - let ref_keys = if context_a_keys.len() > context_b_keys.len() { - context_b_keys - } else { - context_a_keys - }; - - let mut is_overlapping = true; - for key in ref_keys { - let test = (context_a.contains_key(key) && context_b.contains_key(key)) - && (context_a[key] == context_b[key]); - is_overlapping = is_overlapping && test; - - if !test { - break; - } - } - - is_overlapping -} - -pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Value { - match json { - Value::Object(m) => { - let mut m = m.clone(); - for (k, v) in fields { - m.insert(k.clone(), Value::String(v.clone())); - } - Value::Object(m) - } - - v => v.clone(), - } + Scope::new("/experiments") + .service(create) + .service(list_experiments) } #[post("")] async fn create( state: Data, - req: web::Json, -) -> HttpResponse { + req: web::Json, + auth_info: AuthenticationInfo +) -> actix_web::Result, AppError> { use crate::db::schema::experiments::dsl::experiments; let override_keys = &req.override_keys; - let variants = req.variants.to_vec(); + let mut variants = req.variants.to_vec(); let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { - println!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); + println!("Unable to get db connection from pool, error: {e}"); + return Err(AppError { + message: "Could not connect to the database".to_string(), + possible_fix: "Try after sometime".to_string(), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + }); } }; - // Checking if all the variants are overriding the mentioned keys - for variant in &variants { - let overrides = &variant.overrides; - let mut is_valid_variant = false; - - for override_key in override_keys { - let has_override_key = match overrides[override_key] { - Value::Null => false, - _ => true, - }; - is_valid_variant = is_valid_variant && has_override_key; - } - - if !is_valid_variant { - return HttpResponse::BadRequest() - .json( - "{\"message\" : \"all variants should contain the keys mentioned override_keys\"}" - ); + // Checking if experiment has exactly 1 control variant, and + // atleast 1 experimental variant + match check_variant_types(&variants) { + Err(e) => { + return Err(AppError { + message: e.to_string(), + possible_fix: "".to_string(), + status_code: StatusCode::BAD_REQUEST, + }) } + _ => (), } - //traffic_percentage should be max 100/length of variants - //read the envs related to falgs to check the overlapping - let flags = &state.experimentation_flags; + // Checking if all the variants are overriding the mentioned keys + let are_valid_variants = check_variants_override_coverage(&variants, override_keys); + if !are_valid_variants { + return Err(AppError { + message: "all variants should contain the keys mentioned override_keys" + .to_string(), + possible_fix: + "Try including all keys mentioned in override keys in variant overrides" + .to_string(), + status_code: StatusCode::BAD_REQUEST, + }); + } - //check for overlapping context experiments + // Checking if context is a key-value pair map if !req.context.is_object() { - return HttpResponse::BadRequest() - .json("{\"message\": \"context should be a map of key value pairs\" }"); + return Err(AppError { + message: "context should be map of key value pairs".to_string(), + possible_fix: "".to_string(), + status_code: StatusCode::BAD_REQUEST, + }); } - // let context_obj = req.context.as_object().expect("context is not a map"); - let active_experiments: Vec = - get_active_experiments(&mut conn).expect("Failed to get active experiments!"); - - let mut valid_experiment = true; - if !flags.allow_same_keys_overlapping_ctx - || !flags.allow_diff_keys_overlapping_ctx - || !flags.allow_same_keys_non_overlapping_ctx - { - let override_keys_set: HashSet<_> = req.override_keys.iter().collect(); - for active_experiment in active_experiments.iter() { - let are_overlapping = - are_overlapping_contexts(&req.context, &active_experiment.context); - - let have_intersecting_key_set = active_experiment - .override_keys - .iter() - .any(|key| override_keys_set.contains(key)); - - if !flags.allow_diff_keys_overlapping_ctx { - valid_experiment = valid_experiment && !are_overlapping; - } - if !flags.allow_same_keys_overlapping_ctx { - valid_experiment = - valid_experiment && (are_overlapping && !have_intersecting_key_set); - } - if !flags.allow_same_keys_non_overlapping_ctx { - valid_experiment = - valid_experiment && (!are_overlapping && !have_intersecting_key_set); + //traffic_percentage should be max 100/length of variants + // TODO: Add traffic_percentage validation + + // validating experiment against other active experiments based on permission flags + let flags = &state.experimentation_flags; + match validate_experiment(&req, &flags, &mut conn) { + Ok(valid) => { + if !valid { + return Err(AppError { + message: "invalid experiment config".to_string(), + possible_fix: "".to_string(), + status_code: StatusCode::BAD_REQUEST, + }); } } + Err(e) => { + return Err(AppError { + message: e.to_string(), + possible_fix: "".to_string(), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + }); + } } - if !valid_experiment { - return HttpResponse::BadRequest().json("invalid experiment config"); - } - - // create id for experiment + // generating snowflake id for experiment let mut snowflake_generator = state.snowflake_generator.lock().unwrap(); let experiment_id = snowflake_generator.real_time_generate(); //create overrides in CAC, if successfull then create experiment in DB - - for mut variant in variants { - let variant_id = experiment_id.to_string() + &variant.id; + for mut variant in &mut variants { + let variant_id = experiment_id.to_string() + "-" + &variant.id; let fields_to_add = HashMap::from([("variant".to_string(), variant_id.to_string())]); - let updated_context = add_fields_to_json(&req.context, &fields_to_add); + let _updated_cacccontext = add_fields_to_json(&req.context, &fields_to_add); // call cac to send updated_context and req.overrides - // update variant.id to => experiment_id + variant.id + // updating variant.id to => experiment_id + variant.id variant.id = variant_id; } + let AuthenticationInfo(email) = auth_info; let new_experiment = Experiment { id: experiment_id, - created_by: "NA".to_string(), + created_by: email, created_at: Utc::now(), - name: req.name.to_string(), override_keys: req.override_keys.to_vec(), traffic_percentage: req.traffic_percentage, - status: ExperimentStatusType::CREATED, - context: req.context.clone(), - variants: serde_json::to_value(&req.variants).unwrap(), + variants: serde_json::to_value(variants).unwrap(), }; let insert = diesel::insert_into(experiments) .values(&new_experiment) - .execute(&mut conn); + .get_results(&mut conn); match insert { - Ok(_) => return HttpResponse::Created().json(new_experiment), + Ok(mut inserted_experiments) => { + let inserted_experiment = inserted_experiments.remove(0); + let response = ExperimentCreateResponse { + message: "Created".to_string(), + data: inserted_experiment, + }; + + return Ok(Json(response)); + } Err(e) => { println!("Experiment creation failed with error: {e}"); - return HttpResponse::InternalServerError() - .body("Failed to create experiment\n"); + return Err(AppError { + message: "Failed to create experiment".to_string(), + possible_fix: "".to_string(), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + }); } } } @@ -210,8 +171,8 @@ async fn list_experiments( state: Data, filters: Query, ) -> actix_web::Result, AppError> { - let conn = match state.db_pool.get() { - Ok(conn) => &mut conn, + let mut conn = match state.db_pool.get() { + Ok(conn) => conn, Err(e) => { println!("Unable to get db connection from pool, error: {e}"); return Err(AppError { @@ -223,24 +184,30 @@ async fn list_experiments( } }; - let db_result = exp - .filter(status.eq_any(filters.status)) + use crate::db::schema::experiments::dsl::*; + let query = experiments + .filter(status.eq_any(filters.status.clone())) .filter(created_at.ge(filters.from_date)) .filter(created_at.le(filters.to_date)) .limit(filters.count) - .offset((filters.page - 1) * filters.count) - .load(conn); + .offset((filters.page - 1) * filters.count); + + // println!( + // "List filter query: {:?}", + // diesel::debug_query::(&query) + // ); + let db_result = query.load::(&mut conn); - let response = match db_result { - Ok(result) => Json(result), + match db_result { + Ok(response) => return Ok(Json(response)), Err(e) => { return Err(match e { - NotFound => AppError { + diesel::result::Error::NotFound => AppError { message: String::from("No results found"), possible_fix: String::from("Update your filter parameters"), status_code: StatusCode::NOT_FOUND, }, - DatabaseError => AppError { + _ => AppError { message: String::from("Something went wrong"), possible_fix: String::from("Please try again later"), status_code: StatusCode::INTERNAL_SERVER_ERROR, @@ -248,6 +215,4 @@ async fn list_experiments( }) } }; - - return Ok(response); } diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs new file mode 100644 index 000000000..68d1fe212 --- /dev/null +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -0,0 +1,165 @@ +use super::types::{ExperimentCreateRequest, Variant, VariantType}; +use crate::db::models::{Experiment, ExperimentStatusType}; +use diesel::pg::PgConnection; +use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::Value; +use service_utils::service::types::ExperimentationFlags; +use std::collections::{HashMap, HashSet}; + +pub fn check_variant_types(variants: &Vec) -> Result<(), &'static str> { + let mut experimental_variant_cnt = 0; + let mut control_variant_cnt = 0; + + for variant in variants { + match variant.variant_type { + VariantType::CONTROL => { + control_variant_cnt += 1; + } + VariantType::EXPERIMENTAL => { + experimental_variant_cnt += 1; + } + } + } + + if control_variant_cnt > 1 || control_variant_cnt == 0 { + return Err("experiment should have exactly 1 control variant."); + } else if experimental_variant_cnt < 1 { + return Err("experiment should have atlease 1 experimental variant"); + } + + Ok(()) +} + +pub fn are_overlapping_contexts( + context_a_json: &Value, + context_b_json: &Value, +) -> Result { + let context_a = context_a_json + .as_object() + .ok_or("context_a not an object")?; + let context_b = context_b_json + .as_object() + .ok_or("context_b not an object")?; + + let context_a_keys = context_a.keys(); + let context_b_keys = context_b.keys(); + + let ref_keys = if context_a_keys.len() > context_b_keys.len() { + context_b_keys + } else { + context_a_keys + }; + + let mut is_overlapping = true; + for key in ref_keys { + let test = (context_a.contains_key(key) && context_b.contains_key(key)) + && (context_a[key] == context_b[key]); + is_overlapping = is_overlapping && test; + + if !test { + break; + } + } + + Ok(is_overlapping) +} + +pub fn check_variants_override_coverage( + variants: &Vec, + override_keys: &Vec, +) -> bool { + let mut has_complete_coverage = true; + + for variant in variants { + let overrides = &variant.overrides; + let mut is_valid_variant = true; + + for override_key in override_keys { + let has_override_key = match overrides[override_key] { + Value::Null => false, + _ => true, + }; + is_valid_variant = is_valid_variant && has_override_key; + } + + has_complete_coverage = has_complete_coverage && is_valid_variant; + if !has_complete_coverage { + break; + } + } + + has_complete_coverage +} + +pub fn validate_experiment( + experiment: &ExperimentCreateRequest, + flags: &ExperimentationFlags, + conn: &mut PgConnection, +) -> Result { + use crate::db::schema::experiments::dsl::*; + + let created_perdicate = status.eq(ExperimentStatusType::CREATED); + let inprogress_predicate = status.eq(ExperimentStatusType::INPROGRESS); + let active_experiments_filter = + experiments.filter(created_perdicate.or(inprogress_predicate)); + + let active_experiments: Vec = + active_experiments_filter.load(conn).map_err(|e| { + println!("validate_experiment: {e}"); + "Failed to fetch active experiments" + })?; + + let mut valid_experiment = true; + if !flags.allow_same_keys_overlapping_ctx + || !flags.allow_diff_keys_overlapping_ctx + || !flags.allow_same_keys_non_overlapping_ctx + { + let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); + for active_experiment in active_experiments.iter() { + let are_overlapping = + are_overlapping_contexts(&experiment.context, &active_experiment.context) + .map_err(|e| { + println!("validate_experiment: {e}"); + "Failed to validate for overlapping context" + })?; + + let have_intersecting_key_set = active_experiment + .override_keys + .iter() + .any(|key| override_keys_set.contains(key)); + + if !flags.allow_diff_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && !have_intersecting_key_set); + } + if !flags.allow_same_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && have_intersecting_key_set); + } + if !flags.allow_same_keys_non_overlapping_ctx { + valid_experiment = + valid_experiment && !(!are_overlapping && have_intersecting_key_set); + } + + if !valid_experiment { + break; + } + } + } + + Ok(valid_experiment) +} + +pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Value { + match json { + Value::Object(m) => { + let mut m = m.clone(); + for (k, v) in fields { + m.insert(k.clone(), Value::String(v.clone())); + } + Value::Object(m) + } + + v => v.clone(), + } +} diff --git a/crates/experimentation-platform/src/api/experiments/mod.rs b/crates/experimentation-platform/src/api/experiments/mod.rs index 4d863a520..d61ff8763 100644 --- a/crates/experimentation-platform/src/api/experiments/mod.rs +++ b/crates/experimentation-platform/src/api/experiments/mod.rs @@ -1,2 +1,4 @@ pub mod handlers; +pub mod helpers; pub mod types; +pub use handlers::endpoints; diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 99b8f9a70..526182b7f 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,7 +1,12 @@ +use std::fmt; + +use crate::db::models; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{self, IntoDeserializer}, + Deserialize, Serialize, +}; use serde_json::Value; -use crate::db::models; use crate::db::models::ExperimentStatusType; @@ -19,7 +24,7 @@ pub struct Variant { } #[derive(Deserialize)] -pub struct ExperimentCreateReq { +pub struct ExperimentCreateRequest { pub name: String, pub override_keys: Vec, pub traffic_percentage: i32, @@ -29,16 +34,52 @@ pub struct ExperimentCreateReq { } #[derive(Serialize)] -pub struct ExperimentCreateRes { +pub struct ExperimentCreateResponse { pub message: String, pub data: models::Experiment, } #[derive(Deserialize, Debug)] pub struct ListFilters { + #[serde(deserialize_with = "deserialize_stringified_list")] pub status: Vec, pub from_date: DateTime, pub to_date: DateTime, pub page: i64, pub count: i64, } + +pub fn deserialize_stringified_list<'de, D, I>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: de::Deserializer<'de>, + I: de::DeserializeOwned, +{ + struct StringVecVisitor(std::marker::PhantomData); + + impl<'de, I> de::Visitor<'de> for StringVecVisitor + where + I: de::DeserializeOwned, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string containing a list") + } + + fn visit_str(self, v: &str) -> std::result::Result + where + E: de::Error, + { + let mut query_vector = Vec::new(); + for param in v.split(",") { + let p: I = I::deserialize(param.into_deserializer())?; + query_vector.push(p); + } + Ok(query_vector) + } + } + + deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) +} diff --git a/crates/experimentation-platform/src/api/mod.rs b/crates/experimentation-platform/src/api/mod.rs index 8cfa73a69..f6e673e65 100644 --- a/crates/experimentation-platform/src/api/mod.rs +++ b/crates/experimentation-platform/src/api/mod.rs @@ -1,2 +1,2 @@ +pub mod errors; pub mod experiments; -pub mod errors; \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index be6d8a6f7..ccc48b692 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -1,6 +1,6 @@ use crate::db::schema::*; -use chrono::offset::Utc; -use chrono::DateTime; +use chrono::{DateTime, Utc}; + use diesel::{Insertable, Queryable, QueryableByName, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -14,45 +14,21 @@ pub enum ExperimentStatusType { INPROGRESS, } -/*** -impl ToSql for ExperimentStatusType { - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { - match *self { - ExperimentStatusType::CREATED => out.write_all("CREATED"), - ExperimentStatusType::CONCLUDED => out.write_all("CONCLUDED"), - ExperimentStatusType::INPROGRESS => out.write_all("INPROGRESS"), - } - Ok(IsNull::No) - } -} - -impl FromSql for ExperimentStatusType { - fn from_sql(bytes: PgValue<'_>)-> deserialize::Result { - match bytes.as_bytes() { - b"CREATED" => Ok(ExperimentStatusType::CREATED), - b"CONCLUDED" => Ok(ExperimentStatusType::CONCLUDED), - b"INPROGRESS" => Ok(ExperimentStatusType::INPROGRESS), - _ => Err("Unrecognized enum variant".into()), - } - } -} -**/ - -#[derive(QueryableByName, Queryable, Selectable, Insertable, Serialize, Clone)] +#[derive(QueryableByName, Queryable, Selectable, Insertable, Serialize, Clone, Debug)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(id))] pub struct Experiment { - pub id: i64, - pub created_by: String, - pub created_at: DateTime, + pub id: i64, + pub created_at: DateTime, + pub created_by: String, - pub name: String, - pub override_keys: Vec, - pub traffic_percentage: i32, - pub status: ExperimentStatusType, + pub name: String, + pub override_keys: Vec, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, - pub context: Value, - pub variants: Value + pub context: Value, + pub variants: Value, } pub type Experiments = Vec; diff --git a/crates/experimentation-platform/src/lib.rs b/crates/experimentation-platform/src/lib.rs index 0664c941c..037782038 100644 --- a/crates/experimentation-platform/src/lib.rs +++ b/crates/experimentation-platform/src/lib.rs @@ -1,3 +1,2 @@ pub mod api; pub mod db; - From c432c0d2d08487e5544491e000d91c9a4c9ee7ad Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 28 Jul 2023 18:09:12 +0530 Subject: [PATCH 061/352] fix: fixed context overlap check logic --- .../src/api/experiments/helpers.rs | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 68d1fe212..785f37391 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -2,7 +2,7 @@ use super::types::{ExperimentCreateRequest, Variant, VariantType}; use crate::db::models::{Experiment, ExperimentStatusType}; use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; -use serde_json::Value; +use serde_json::{Map, Value}; use service_utils::service::types::ExperimentationFlags; use std::collections::{HashMap, HashSet}; @@ -30,30 +30,79 @@ pub fn check_variant_types(variants: &Vec) -> Result<(), &'static str> Ok(()) } +pub fn extract_dimensions( + context_json: &Value, +) -> Result, &'static str> { + // Assuming max 2-level nesting in context json logic + let context = context_json + .as_object() + .ok_or("extract_dimensions: context not an object")?; + + let conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or("extract_dimension: failed parsing conditions as an array")? + .clone(), + None => vec![context_json.clone()], + }; + + let mut dimension_tuples = Vec::new(); + for condition in &conditions { + let condition_obj = condition + .as_object() + .ok_or("extract_dimensions: failed to parse condition as an object")?; + let operators = condition_obj.keys(); + + for operator in operators { + let operands = condition_obj[operator] + .as_array() + .ok_or("extract_dimension: failed to parse operands as an arrays")?; + + let variable_name = operands + .get(0) // getting first element which should contain an object with property `var` with string value + .ok_or( + "extract_dimension: failed to get variable name from operands list", + )? + .as_object() // parsing json value as an object/map + .ok_or("extract_dimension: failed to parse variable as an object")? + .get("var") // accessing `var` from object/map which contains variable name + .ok_or("extract_dimension: var property not present in variable object")? + .as_str() // parsing json value as raw string + .ok_or("extract_dimension: var propery value is not a string")?; + + let variable_value = operands + .get(1) // getting second element which should be the value of the variable + .ok_or( + "extract_dimension: failed to get variable value from operands list", + )?; + + dimension_tuples.push((String::from(variable_name), variable_value.clone())); + } + } + + Ok(Map::from_iter(dimension_tuples)) +} + pub fn are_overlapping_contexts( - context_a_json: &Value, - context_b_json: &Value, + context_a: &Value, + context_b: &Value, ) -> Result { - let context_a = context_a_json - .as_object() - .ok_or("context_a not an object")?; - let context_b = context_b_json - .as_object() - .ok_or("context_b not an object")?; + let dimensions_a = extract_dimensions(context_a)?; + let dimensions_b = extract_dimensions(context_b)?; - let context_a_keys = context_a.keys(); - let context_b_keys = context_b.keys(); + let dim_a_keys = dimensions_a.keys(); + let dim_b_keys = dimensions_b.keys(); - let ref_keys = if context_a_keys.len() > context_b_keys.len() { - context_b_keys + let ref_keys = if dim_a_keys.len() > dim_b_keys.len() { + dim_b_keys } else { - context_a_keys + dim_a_keys }; let mut is_overlapping = true; for key in ref_keys { - let test = (context_a.contains_key(key) && context_b.contains_key(key)) - && (context_a[key] == context_b[key]); + let test = (dimensions_a.contains_key(key) && dimensions_b.contains_key(key)) + && (dimensions_a[key] == dimensions_b[key]); is_overlapping = is_overlapping && test; if !test { From aa7690a3881ec7081612aa5be6e6af4d3507bf06 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Sun, 30 Jul 2023 19:44:47 +0530 Subject: [PATCH 062/352] fix: added last_modified column and indexes --- .../migrations/2023-07-14-093533_create_experiments/up.sql | 6 +++++- .../src/api/experiments/handlers.rs | 3 ++- crates/experimentation-platform/src/db/models.rs | 1 + crates/experimentation-platform/src/db/schema.rs | 1 + crates/experimentation-platform/src/schema.patch | 6 +++--- makefile | 1 + 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql index 20f1d9183..5fd0c9482 100644 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql @@ -5,6 +5,7 @@ CREATE TABLE experiments ( id BIGINT PRIMARY KEY, created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by TEXT NOT NULL, + last_modified timestamp WITH time zone, name TEXT NOT NULL, override_keys TEXT[] NOT NULL, @@ -16,4 +17,7 @@ CREATE TABLE experiments ( constraint check_override_keys_not_null check ( array_position(override_keys, null) is null and override_keys <> '{}' ) -) +); + +CREATE INDEX experiment_created_date_index ON experiments (created_at) INCLUDE (id); +CREATE INDEX experiment_last_modified_index ON experiments (last_modified) INCLUDE (id, created_at); diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index e27728000..b54bdb19c 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -133,6 +133,7 @@ async fn create( id: experiment_id, created_by: email, created_at: Utc::now(), + last_modified: Option::None, name: req.name.to_string(), override_keys: req.override_keys.to_vec(), traffic_percentage: req.traffic_percentage, @@ -166,7 +167,7 @@ async fn create( } } -#[get("/list")] +#[get("")] async fn list_experiments( state: Data, filters: Query, diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index ccc48b692..04dbf7e3b 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -21,6 +21,7 @@ pub struct Experiment { pub id: i64, pub created_at: DateTime, pub created_by: String, + pub last_modified: Option>, pub name: String, pub override_keys: Vec, diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 3d4f52bfa..887ba9e08 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -14,6 +14,7 @@ diesel::table! { id -> Int8, created_at -> Timestamptz, created_by -> Text, + last_modified -> Nullable, name -> Text, override_keys -> Array, status -> ExperimentStatusType, diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index 72c02779a..d0f825c01 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -1,13 +1,13 @@ diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs -index 670548b..3d4f52b 100644 +index fc346c1..887ba9e 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs -@@ -12,13 +12,13 @@ diesel::table! { - +@@ -13,13 +13,13 @@ diesel::table! { experiments (id) { id -> Int8, created_at -> Timestamptz, created_by -> Text, + last_modified -> Nullable, name -> Text, - override_keys -> Array>, + override_keys -> Array, diff --git a/makefile b/makefile index b15ae7be7..4c2b2668e 100644 --- a/makefile +++ b/makefile @@ -35,6 +35,7 @@ run: sleep 10 #TODO move this sleep to aws cli list-keys command instead cargo build --color always diesel migration run --config-file=crates/context-aware-config/diesel.toml + diesel migration run --config-file=crates/experimentation-platform/diesel.toml source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always From 023f260651d253f3e156f42fc0448059e317508b Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Tue, 1 Aug 2023 18:55:40 +0530 Subject: [PATCH 063/352] fix: moved tables and types under cac_v1 schema --- crates/experimentation-platform/diesel.toml | 1 + .../up.sql | 18 ++-- .../src/api/experiments/handlers.rs | 4 +- .../src/api/experiments/helpers.rs | 2 +- .../experimentation-platform/src/db/models.rs | 4 +- .../experimentation-platform/src/db/schema.rs | 90 ++++++++++++++----- .../experimentation-platform/src/schema.patch | 56 ++++++++---- makefile | 2 +- 8 files changed, 126 insertions(+), 51 deletions(-) diff --git a/crates/experimentation-platform/diesel.toml b/crates/experimentation-platform/diesel.toml index da017c980..0c229d58d 100644 --- a/crates/experimentation-platform/diesel.toml +++ b/crates/experimentation-platform/diesel.toml @@ -1,5 +1,6 @@ [print_schema] file = "src/db/schema.rs" +schema = "cac_v1" patch_file = "src/schema.patch" [migrations_directory] diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql index 5fd0c9482..7f759cf70 100644 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql @@ -1,23 +1,21 @@ -- Your SQL goes here -CREATE TYPE experiment_status_type as enum ('CREATED', 'CONCLUDED', 'INPROGRESS'); +CREATE TYPE cac_v1.experiment_status_type as enum ('CREATED', 'CONCLUDED', 'INPROGRESS'); +CREATE DOMAIN cac_v1.not_null_text as TEXT NOT NULL; -CREATE TABLE experiments ( +CREATE TABLE cac_v1.experiments ( id BIGINT PRIMARY KEY, created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by TEXT NOT NULL, last_modified timestamp WITH time zone, name TEXT NOT NULL, - override_keys TEXT[] NOT NULL, - status experiment_status_type NOT NULL, + override_keys cac_v1.not_null_text [] NOT NULL, + status cac_v1.experiment_status_type NOT NULL, traffic_percentage INTEGER NOT NULL CHECK (traffic_percentage >= 0), context JSON NOT NULL, - variants JSON NOT NULL, - - constraint check_override_keys_not_null - check ( array_position(override_keys, null) is null and override_keys <> '{}' ) + variants JSON NOT NULL ); -CREATE INDEX experiment_created_date_index ON experiments (created_at) INCLUDE (id); -CREATE INDEX experiment_last_modified_index ON experiments (last_modified) INCLUDE (id, created_at); +CREATE INDEX experiment_created_date_index ON cac_v1.experiments (created_at) INCLUDE (id); +CREATE INDEX experiment_last_modified_index ON cac_v1.experiments (last_modified) INCLUDE (id, created_at); diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index b54bdb19c..fd138fb85 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -35,7 +35,7 @@ async fn create( req: web::Json, auth_info: AuthenticationInfo ) -> actix_web::Result, AppError> { - use crate::db::schema::experiments::dsl::experiments; + use crate::db::schema::cac_v1::experiments::dsl::experiments; let override_keys = &req.override_keys; let mut variants = req.variants.to_vec(); @@ -185,7 +185,7 @@ async fn list_experiments( } }; - use crate::db::schema::experiments::dsl::*; + use crate::db::schema::cac_v1::experiments::dsl::*; let query = experiments .filter(status.eq_any(filters.status.clone())) .filter(created_at.ge(filters.from_date)) diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 785f37391..1d1afd384 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -145,7 +145,7 @@ pub fn validate_experiment( flags: &ExperimentationFlags, conn: &mut PgConnection, ) -> Result { - use crate::db::schema::experiments::dsl::*; + use crate::db::schema::cac_v1::experiments::dsl::*; let created_perdicate = status.eq(ExperimentStatusType::CREATED); let inprogress_predicate = status.eq(ExperimentStatusType::INPROGRESS); diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index 04dbf7e3b..a17f840b3 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -1,4 +1,4 @@ -use crate::db::schema::*; +use crate::db::schema::cac_v1::*; use chrono::{DateTime, Utc}; use diesel::{Insertable, Queryable, QueryableByName, Selectable}; @@ -7,7 +7,7 @@ use serde_json::Value; #[derive(Debug, Clone, Copy, Deserialize, Serialize, diesel_derive_enum::DbEnum)] #[DbValueStyle = "UPPERCASE"] -#[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] +#[ExistingTypePath = "crate::db::schema::cac_v1::sql_types::ExperimentStatusType"] pub enum ExperimentStatusType { CREATED, CONCLUDED, diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 887ba9e08..c6ac1ad79 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -1,25 +1,75 @@ // @generated automatically by Diesel CLI. -pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "experiment_status_type"))] - pub struct ExperimentStatusType; -} +pub mod cac_v1 { + pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] + pub struct DimensionType; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] + pub struct ExperimentStatusType; + } + + diesel::table! { + cac_v1.contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + priority -> Int4, + #[sql_name = "override"] + override_ -> Json, + } + } + + diesel::table! { + cac_v1.default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } + } + + diesel::table! { + use diesel::sql_types::*; + use super::sql_types::DimensionType; + + cac_v1.dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + #[sql_name = "type"] + type_ -> DimensionType, + created_at -> Timestamptz, + created_by -> Varchar, + } + } + + diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ExperimentStatusType; -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::ExperimentStatusType; - - experiments (id) { - id -> Int8, - created_at -> Timestamptz, - created_by -> Text, - last_modified -> Nullable, - name -> Text, - override_keys -> Array, - status -> ExperimentStatusType, - traffic_percentage -> Int4, - context -> Json, - variants -> Json, + cac_v1.experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + last_modified -> Nullable, + name -> Text, + override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + } } + + diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, + experiments, + ); } diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index d0f825c01..b1c1c9218 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -1,19 +1,45 @@ diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs -index fc346c1..887ba9e 100644 +index ca23ca2..e6083a7 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs -@@ -13,13 +13,13 @@ diesel::table! { - experiments (id) { - id -> Int8, - created_at -> Timestamptz, - created_by -> Text, - last_modified -> Nullable, - name -> Text, -- override_keys -> Array>, -+ override_keys -> Array, - status -> ExperimentStatusType, - traffic_percentage -> Int4, - context -> Json, - variants -> Json, +@@ -6,16 +6,12 @@ pub mod cac_v1 { + #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] + pub struct DimensionType; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] + pub struct ExperimentStatusType; +- +- #[derive(diesel::sql_types::SqlType)] +- #[diesel(postgres_type(name = "not_null_text", schema = "cac_v1"))] +- pub struct NotNullText; + } + + diesel::table! { + cac_v1.contexts (id) { + id -> Varchar, + value -> Json, +@@ -51,22 +47,21 @@ pub mod cac_v1 { + created_by -> Varchar, + } + } + + diesel::table! { + use diesel::sql_types::*; +- use super::sql_types::NotNullText; + use super::sql_types::ExperimentStatusType; + + cac_v1.experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + last_modified -> Nullable, + name -> Text, +- override_keys -> Array>, ++ override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + } } - } diff --git a/makefile b/makefile index 4c2b2668e..5d31a68cd 100644 --- a/makefile +++ b/makefile @@ -33,9 +33,9 @@ run: #NOTE need to sleep here because locastack takes some time to internally #populate the kms keyId sleep 10 #TODO move this sleep to aws cli list-keys command instead - cargo build --color always diesel migration run --config-file=crates/context-aware-config/diesel.toml diesel migration run --config-file=crates/experimentation-platform/diesel.toml + cargo build --color always source ./docker-compose/localstack/export_cyphers.sh && \ cargo run --color always From 0cbdcb9cf3794a7de1fa5a9bd19779a2dd4f9787 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Tue, 1 Aug 2023 19:11:29 +0530 Subject: [PATCH 064/352] fix: calling cac apis for creating context --- .env.example | 1 + Cargo.lock | 1252 +++++++++++++++-- crates/context-aware-config/src/main.rs | 2 + crates/experimentation-platform/Cargo.toml | 1 + .../src/api/experiments/handlers.rs | 139 +- .../src/api/experiments/helpers.rs | 44 +- .../src/api/experiments/queries.rs | 0 .../src/api/experiments/types.rs | 34 +- crates/service-utils/src/service/types.rs | 1 + 9 files changed, 1238 insertions(+), 236 deletions(-) delete mode 100644 crates/experimentation-platform/src/api/experiments/queries.rs diff --git a/.env.example b/.env.example index 6dea774b1..a13da479d 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ DB_HOST=127.0.0.1:5432 DB_NAME=config APP_ENV=DEV ADMIN_TOKEN="12345678" +CAC_HOST="http://localhost:8080" ALLOW_SAME_KEYS_OVERLAPPING_CTX=true ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true diff --git a/Cargo.lock b/Cargo.lock index 9755d378d..d25fdadc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "actix-rt", "actix_derive", "bitflags 1.3.2", - "bytes", + "bytes 1.4.0", "crossbeam-channel", "futures-core", "futures-sink", @@ -19,10 +19,10 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", - "smallvec", - "tokio", + "smallvec 1.10.0", + "tokio 1.28.1", "tokio-util", ] @@ -33,13 +33,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" dependencies = [ "bitflags 1.3.2", - "bytes", + "bytes 1.4.0", "futures-core", "futures-sink", "log", "memchr", "pin-project-lite", - "tokio", + "tokio 1.28.1", "tokio-util", ] @@ -57,26 +57,26 @@ dependencies = [ "base64 0.21.2", "bitflags 1.3.2", "brotli", - "bytes", + "bytes 1.4.0", "bytestring", "derive_more", "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.18", + "http 0.2.9", "httparse", "httpdate", - "itoa", + "itoa 1.0.6", "language-tags", "local-channel", "mime", - "percent-encoding", + "percent-encoding 2.2.0", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", - "smallvec", - "tokio", + "smallvec 1.10.0", + "tokio 1.28.1", "tokio-util", "tracing", "zstd", @@ -99,7 +99,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" dependencies = [ "bytestring", - "http", + "http 0.2.9", "regex", "serde", "tracing", @@ -112,7 +112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" dependencies = [ "futures-core", - "tokio", + "tokio 1.28.1", ] [[package]] @@ -126,10 +126,10 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.6", "num_cpus", "socket2", - "tokio", + "tokio 1.28.1", "tracing", ] @@ -170,16 +170,16 @@ dependencies = [ "actix-utils", "actix-web-codegen", "ahash 0.7.6", - "bytes", + "bytes 1.4.0", "bytestring", - "cfg-if", - "cookie", + "cfg-if 1.0.0", + "cookie 0.16.2", "derive_more", "encoding_rs", "futures-core", "futures-util", - "http", - "itoa", + "http 0.2.9", + "itoa 1.0.6", "language-tags", "log", "mime", @@ -188,11 +188,11 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded", - "smallvec", + "serde_urlencoded 0.7.1", + "smallvec 1.10.0", "socket2", "time 0.3.21", - "url", + "url 2.3.1", ] [[package]] @@ -218,6 +218,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -241,7 +250,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "getrandom", "once_cell", "serde", @@ -367,7 +376,16 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", ] [[package]] @@ -376,6 +394,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.13.1" @@ -424,7 +466,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if", + "cfg-if 1.0.0", "constant_time_eq", "digest 0.10.6", ] @@ -486,6 +528,17 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "either", + "iovec", +] + [[package]] name = "bytes" version = "1.4.0" @@ -498,7 +551,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" dependencies = [ - "bytes", + "bytes 1.4.0", ] [[package]] @@ -510,6 +563,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -529,7 +588,7 @@ dependencies = [ "serde", "time 0.1.45", "wasm-bindgen", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -574,6 +633,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -604,7 +672,7 @@ dependencies = [ "actix-web", "base64 0.21.2", "blake3", - "bytes", + "bytes 1.4.0", "chrono", "derive_more", "diesel", @@ -615,7 +683,7 @@ dependencies = [ "json-patch", "jsonschema", "log", - "reqwest", + "reqwest 0.11.18", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -635,17 +703,45 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" +dependencies = [ + "time 0.1.45", + "url 1.7.2", +] + [[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "percent-encoding", + "percent-encoding 2.2.0", "time 0.3.21", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" +dependencies = [ + "cookie 0.12.0", + "failure", + "idna 0.1.5", + "log", + "publicsuffix", + "serde", + "serde_json", + "time 0.1.45", + "try_from", + "url 1.7.2", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -677,7 +773,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -686,8 +782,56 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.15", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg 1.1.0", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.1.0", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -696,7 +840,7 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -772,7 +916,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -786,7 +930,7 @@ dependencies = [ "byteorder", "chrono", "diesel_derives", - "itoa", + "itoa 1.0.6", "pq-sys", "r2d2", "serde_json", @@ -852,7 +996,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "dirs-sys-next", ] @@ -864,7 +1008,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -873,13 +1017,25 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -929,6 +1085,7 @@ dependencies = [ "dotenv", "env_logger", "log", + "reqwest 0.9.24", "rs-snowflake", "serde", "serde_json", @@ -938,6 +1095,28 @@ dependencies = [ "uuid 0.8.2", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -964,7 +1143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -994,7 +1173,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "percent-encoding", + "percent-encoding 2.2.0", ] [[package]] @@ -1007,6 +1186,34 @@ dependencies = [ "num", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.28" @@ -1038,6 +1245,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +dependencies = [ + "futures 0.1.31", + "num_cpus", +] + [[package]] name = "futures-executor" version = "0.3.28" @@ -1112,28 +1329,52 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +dependencies = [ + "byteorder", + "bytes 0.4.12", + "fnv", + "futures 0.1.31", + "http 0.1.21", + "indexmap", + "log", + "slab", + "string", + "tokio-io", +] + [[package]] name = "h2" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" dependencies = [ - "bytes", + "bytes 1.4.0", "fnv", "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap", "slab", - "tokio", + "tokio 1.28.1", "tokio-util", "tracing", ] @@ -1190,15 +1431,38 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "http" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +dependencies = [ + "bytes 0.4.12", + "fnv", + "itoa 0.4.8", +] + [[package]] name = "http" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes", + "bytes 1.4.0", "fnv", - "itoa", + "itoa 1.0.6", +] + +[[package]] +name = "http-body" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "http 0.1.21", + "tokio-buf", ] [[package]] @@ -1207,8 +1471,8 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", - "http", + "bytes 1.4.0", + "http 0.2.9", "pin-project-lite", ] @@ -1230,28 +1494,58 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.12.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c843caf6296fc1f93444735205af9ed4e109a539005abb2564ae1d6fad34c52" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "futures-cpupool", + "h2 0.1.26", + "http 0.1.21", + "http-body 0.1.0", + "httparse", + "iovec", + "itoa 0.4.8", + "log", + "net2", + "rustc_version 0.2.3", + "time 0.1.45", + "tokio 0.1.22", + "tokio-buf", + "tokio-executor", + "tokio-io", + "tokio-reactor", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "want 0.2.0", +] + [[package]] name = "hyper" version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ - "bytes", + "bytes 1.4.0", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.18", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", - "itoa", + "itoa 1.0.6", "pin-project-lite", "socket2", - "tokio", + "tokio 1.28.1", "tower-service", "tracing", - "want", + "want 0.3.0", ] [[package]] @@ -1260,23 +1554,36 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.26", "rustls", - "tokio", + "tokio 1.28.1", "tokio-rustls", ] +[[package]] +name = "hyper-tls" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "hyper 0.12.36", + "native-tls", + "tokio-io", +] + [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", - "hyper", + "bytes 1.4.0", + "hyper 0.14.26", "native-tls", - "tokio", + "tokio 1.28.1", "tokio-native-tls", ] @@ -1304,6 +1611,28 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.3.0" @@ -1320,7 +1649,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown", ] @@ -1330,7 +1659,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1344,6 +1673,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.7.2" @@ -1371,6 +1709,12 @@ dependencies = [ "nom", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.6" @@ -1422,21 +1766,31 @@ dependencies = [ "fraction", "getrandom", "iso8601", - "itoa", + "itoa 1.0.6", "memchr", "num-cmp", "once_cell", - "parking_lot", - "percent-encoding", + "parking_lot 0.12.1", + "percent-encoding 2.2.0", "regex", - "reqwest", + "reqwest 0.11.18", "serde", "serde_json", "time 0.3.21", - "url", + "url 2.3.1", "uuid 1.3.4", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1488,13 +1842,22 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -1504,9 +1867,21 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.9.1" @@ -1524,18 +1899,46 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1547,20 +1950,51 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "miow", + "net2", + "slab", + "winapi 0.2.8", ] [[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", @@ -1575,6 +2009,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "7.1.3" @@ -1605,7 +2050,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1631,7 +2076,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-traits", ] @@ -1641,7 +2086,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1652,7 +2097,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-bigint", "num-integer", "num-traits", @@ -1664,7 +2109,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -1677,6 +2122,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -1696,7 +2150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -1733,14 +2187,40 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.3", + "rustc_version 0.2.3", +] + [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api", - "parking_lot_core", + "lock_api 0.4.9", + "parking_lot_core 0.9.7", +] + +[[package]] +name = "parking_lot_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version 0.2.3", + "smallvec 0.6.14", + "winapi 0.3.9", ] [[package]] @@ -1749,10 +2229,10 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall 0.2.16", - "smallvec", + "smallvec 1.10.0", "windows-sys 0.45.0", ] @@ -1762,6 +2242,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1810,6 +2296,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "publicsuffix" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" +dependencies = [ + "idna 0.2.3", + "url 2.3.1", +] + [[package]] name = "quote" version = "1.0.27" @@ -1826,10 +2322,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot", + "parking_lot 0.12.1", "scheduled-thread-pool", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.8.5" @@ -1837,8 +2352,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] @@ -1848,9 +2373,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", ] +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -1860,6 +2400,83 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1906,6 +2523,40 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "reqwest" +version = "0.9.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" +dependencies = [ + "base64 0.10.1", + "bytes 0.4.12", + "cookie 0.12.0", + "cookie_store", + "encoding_rs", + "flate2", + "futures 0.1.31", + "http 0.1.21", + "hyper 0.12.36", + "hyper-tls 0.3.2", + "log", + "mime", + "mime_guess", + "native-tls", + "serde", + "serde_json", + "serde_urlencoded 0.5.5", + "time 0.1.45", + "tokio 0.1.22", + "tokio-executor", + "tokio-io", + "tokio-threadpool", + "tokio-timer", + "url 1.7.2", + "uuid 0.7.4", + "winreg 0.6.2", +] + [[package]] name = "reqwest" version = "0.11.18" @@ -1913,39 +2564,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ "base64 0.21.2", - "bytes", + "bytes 1.4.0", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.18", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.26", "hyper-rustls", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", - "percent-encoding", + "percent-encoding 2.2.0", "pin-project-lite", "rustls", "rustls-pemfile", "serde", "serde_json", - "serde_urlencoded", - "tokio", + "serde_urlencoded 0.7.1", + "tokio 1.28.1", "tokio-native-tls", "tokio-rustls", "tower-service", - "url", + "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -1960,7 +2611,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1977,20 +2628,20 @@ checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" dependencies = [ "async-trait", "base64 0.13.1", - "bytes", + "bytes 1.4.0", "crc32fast", - "futures", - "http", - "hyper", - "hyper-tls", + "futures 0.3.28", + "http 0.2.9", + "hyper 0.14.26", + "hyper-tls 0.5.0", "lazy_static", "log", "rusoto_credential", "rusoto_signature", - "rustc_version", + "rustc_version 0.4.0", "serde", "serde_json", - "tokio", + "tokio 1.28.1", "xml-rs", ] @@ -2003,12 +2654,12 @@ dependencies = [ "async-trait", "chrono", "dirs-next", - "futures", - "hyper", + "futures 0.3.28", + "hyper 0.14.26", "serde", "serde_json", "shlex", - "tokio", + "tokio 1.28.1", "zeroize", ] @@ -2019,8 +2670,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" dependencies = [ "async-trait", - "bytes", - "futures", + "bytes 1.4.0", + "futures 0.3.28", "rusoto_core", "serde", "serde_json", @@ -2033,23 +2684,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" dependencies = [ "base64 0.13.1", - "bytes", + "bytes 1.4.0", "chrono", "digest 0.9.0", - "futures", + "futures 0.3.28", "hex", "hmac", - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.26", "log", "md-5", - "percent-encoding", + "percent-encoding 2.2.0", "pin-project-lite", "rusoto_credential", - "rustc_version", + "rustc_version 0.4.0", "serde", "sha2", - "tokio", + "tokio 1.28.1", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", ] [[package]] @@ -2058,7 +2724,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.17", ] [[package]] @@ -2133,7 +2799,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -2181,12 +2847,27 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.162" @@ -2213,11 +2894,23 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "itoa", + "itoa 1.0.6", "ryu", "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" +dependencies = [ + "dtoa", + "itoa 0.4.8", + "serde", + "url 1.7.2", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2225,7 +2918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.6", "ryu", "serde", ] @@ -2237,7 +2930,7 @@ dependencies = [ "actix", "actix-web", "base64 0.21.2", - "bytes", + "bytes 1.4.0", "diesel", "dotenv", "jsonschema", @@ -2254,7 +2947,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.6", ] @@ -2266,7 +2959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -2293,7 +2986,16 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ - "autocfg", + "autocfg 1.1.0", +] + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", ] [[package]] @@ -2309,7 +3011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2318,6 +3020,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "string" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +dependencies = [ + "bytes 0.4.12", +] + [[package]] name = "strsim" version = "0.10.0" @@ -2371,14 +3082,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ - "autocfg", - "cfg-if", + "autocfg 1.1.0", + "cfg-if 1.0.0", "fastrand", "redox_syscall 0.3.5", "rustix", @@ -2422,7 +3145,7 @@ checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2431,7 +3154,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ - "itoa", + "itoa 1.0.6", "serde", "time-core", "time-macros", @@ -2467,18 +3190,37 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "mio 0.6.23", + "num_cpus", + "tokio-current-thread", + "tokio-executor", + "tokio-io", + "tokio-reactor", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", +] + [[package]] name = "tokio" version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ - "autocfg", - "bytes", + "autocfg 1.1.0", + "bytes 1.4.0", "libc", - "mio", + "mio 0.8.6", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2486,6 +3228,48 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-buf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" +dependencies = [ + "bytes 0.4.12", + "either", + "futures 0.1.31", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures 0.1.31", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + [[package]] name = "tokio-macros" version = "2.1.0" @@ -2504,7 +3288,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio", + "tokio 1.28.1", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "mio 0.6.23", + "num_cpus", + "parking_lot 0.9.0", + "slab", + "tokio-executor", + "tokio-io", + "tokio-sync", ] [[package]] @@ -2514,7 +3317,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", - "tokio", + "tokio 1.28.1", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures 0.1.31", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "mio 0.6.23", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "num_cpus", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "slab", + "tokio-executor", ] [[package]] @@ -2523,11 +3379,11 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ - "bytes", + "bytes 1.4.0", "futures-core", "futures-sink", "pin-project-lite", - "tokio", + "tokio 1.28.1", "tracing", ] @@ -2543,7 +3399,7 @@ version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-core", @@ -2573,12 +3429,30 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "try_from" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" +dependencies = [ + "cfg-if 0.1.10", +] + [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2606,12 +3480,29 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.3.1" @@ -2619,8 +3510,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna", - "percent-encoding", + "idna 0.3.0", + "percent-encoding 2.2.0", ] [[package]] @@ -2635,6 +3526,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +dependencies = [ + "rand 0.6.5", +] + [[package]] name = "uuid" version = "0.8.2" @@ -2663,6 +3563,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" +dependencies = [ + "futures 0.1.31", + "log", + "try-lock", +] + [[package]] name = "want" version = "0.3.0" @@ -2691,7 +3602,7 @@ version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -2716,7 +3627,7 @@ version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -2780,6 +3691,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -2790,6 +3707,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2802,7 +3725,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2967,13 +3890,32 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winreg" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index a9f6c1cac..5186c0c44 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -28,6 +28,7 @@ async fn main() -> Result<()> { env_logger::init(); let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); + let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); /****** EXPERIMENTATION PLATFORM ENVs *********/ @@ -50,6 +51,7 @@ async fn main() -> Result<()> { db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), admin_token: admin_token.to_owned(), + cac_host: cac_host.to_owned(), experimentation_flags: ExperimentationFlags { allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 6ee35fecf..4b75deb61 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -31,3 +31,4 @@ chrono = { version = "0.4", features = ["serde"] } diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } +reqwest = {version = "0.9.11"} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index fd138fb85..de389657d 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -7,16 +7,18 @@ use actix_web::{ }; use chrono::Utc; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use std::collections::HashMap; -use service_utils::service::types::{AppState, AuthenticationInfo}; +use service_utils::service::types::{AppState, AuthenticationInfo, DbConnection}; use super::{ helpers::{ - add_fields_to_json, check_variant_types, check_variants_override_coverage, - validate_experiment, + add_variant_dimension_to_ctx, check_variant_types, + check_variants_override_coverage, validate_experiment, + }, + types::{ + ContextAction, ContextPutReq, ContextPutResp, ExperimentCreateRequest, + ExperimentCreateResponse, }, - types::{ExperimentCreateRequest, ExperimentCreateResponse}, }; use crate::{ api::{errors::AppError, experiments::types::ListFilters}, @@ -33,58 +35,33 @@ pub fn endpoints() -> Scope { async fn create( state: Data, req: web::Json, - auth_info: AuthenticationInfo -) -> actix_web::Result, AppError> { + auth_info: AuthenticationInfo, + db_conn: DbConnection, +) -> actix_web::Result> { use crate::db::schema::cac_v1::experiments::dsl::experiments; + let DbConnection(mut conn) = db_conn; let override_keys = &req.override_keys; let mut variants = req.variants.to_vec(); - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - println!("Unable to get db connection from pool, error: {e}"); - return Err(AppError { - message: "Could not connect to the database".to_string(), - possible_fix: "Try after sometime".to_string(), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }); - } - }; - // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant - match check_variant_types(&variants) { - Err(e) => { - return Err(AppError { - message: e.to_string(), - possible_fix: "".to_string(), - status_code: StatusCode::BAD_REQUEST, - }) - } - _ => (), - } + check_variant_types(&variants) + .map_err(|e| actix_web::error::ErrorBadRequest(e.to_string()))?; // Checking if all the variants are overriding the mentioned keys let are_valid_variants = check_variants_override_coverage(&variants, override_keys); if !are_valid_variants { - return Err(AppError { - message: "all variants should contain the keys mentioned override_keys" - .to_string(), - possible_fix: - "Try including all keys mentioned in override keys in variant overrides" - .to_string(), - status_code: StatusCode::BAD_REQUEST, - }); + return Err(actix_web::error::ErrorBadRequest( + "all variants should contain the keys mentioned override_keys".to_string(), + )); } // Checking if context is a key-value pair map if !req.context.is_object() { - return Err(AppError { - message: "context should be map of key value pairs".to_string(), - possible_fix: "".to_string(), - status_code: StatusCode::BAD_REQUEST, - }); + return Err(actix_web::error::ErrorBadRequest( + "context should be map of key value pairs".to_string(), + )); } //traffic_percentage should be max 100/length of variants @@ -95,19 +72,13 @@ async fn create( match validate_experiment(&req, &flags, &mut conn) { Ok(valid) => { if !valid { - return Err(AppError { - message: "invalid experiment config".to_string(), - possible_fix: "".to_string(), - status_code: StatusCode::BAD_REQUEST, - }); + return Err(actix_web::error::ErrorBadRequest( + "invalid experiment config".to_string(), + )); } } - Err(e) => { - return Err(AppError { - message: e.to_string(), - possible_fix: "".to_string(), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }); + Err(_) => { + return Err(actix_web::error::ErrorInternalServerError("")); } } @@ -116,18 +87,55 @@ async fn create( let experiment_id = snowflake_generator.real_time_generate(); //create overrides in CAC, if successfull then create experiment in DB + let mut cac_operations: Vec = vec![]; for mut variant in &mut variants { let variant_id = experiment_id.to_string() + "-" + &variant.id; - let fields_to_add = - HashMap::from([("variant".to_string(), variant_id.to_string())]); - - let _updated_cacccontext = add_fields_to_json(&req.context, &fields_to_add); - // call cac to send updated_context and req.overrides // updating variant.id to => experiment_id + variant.id - variant.id = variant_id; + variant.id = variant_id.to_string(); + + let updated_cacccontext = + add_variant_dimension_to_ctx(&req.context, variant_id.to_string()) + .map_err(|_| actix_web::error::ErrorInternalServerError(""))?; + + let payload = ContextPutReq { + context: updated_cacccontext + .as_object() + .ok_or(actix_web::error::ErrorInternalServerError(""))? + .clone(), + r#override: variant.overrides.clone(), + }; + cac_operations.push(ContextAction::PUT(payload)); + } + + // creating variants' context in CAC + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + + let created_contexts: Vec = http_client + .put(&url) + .bearer_auth(&state.admin_token) + .json(&cac_operations) + .send() + .map_err(|e| { + println!("failed to create contexts in cac: {e}"); + actix_web::error::ErrorInternalServerError("") + })? + .json::>() + .map_err(|e| { + println!("failed to parse response: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; + + // updating variants with context and override ids + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; + + variants[i].context_id = Some(created_context.context_id.clone()); + variants[i].override_id = Some(created_context.override_id.clone()); } + // inserting experiment in db let AuthenticationInfo(email) = auth_info; let new_experiment = Experiment { id: experiment_id, @@ -148,21 +156,18 @@ async fn create( match insert { Ok(mut inserted_experiments) => { - let inserted_experiment = inserted_experiments.remove(0); + let inserted_experiment: Experiment = inserted_experiments.remove(0); let response = ExperimentCreateResponse { - message: "Created".to_string(), - data: inserted_experiment, + experiment_id: inserted_experiment.id, }; return Ok(Json(response)); } Err(e) => { println!("Experiment creation failed with error: {e}"); - return Err(AppError { - message: "Failed to create experiment".to_string(), - possible_fix: "".to_string(), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to create experiment".to_string(), + )); } } } diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 1d1afd384..5d33f1c8e 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -4,7 +4,7 @@ use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value}; use service_utils::service::types::ExperimentationFlags; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; pub fn check_variant_types(variants: &Vec) -> Result<(), &'static str> { let mut experimental_variant_cnt = 0; @@ -199,16 +199,38 @@ pub fn validate_experiment( Ok(valid_experiment) } -pub fn add_fields_to_json(json: &Value, fields: &HashMap) -> Value { - match json { - Value::Object(m) => { - let mut m = m.clone(); - for (k, v) in fields { - m.insert(k.clone(), Value::String(v.clone())); - } - Value::Object(m) - } +pub fn add_variant_dimension_to_ctx( + context_json: &Value, + variant: String, +) -> Result { + let context = context_json + .as_object() + .ok_or("extract_dimensions: context not an object")?; + + let mut conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or("extract_dimension: failed parsing conditions as an array")? + .clone(), + None => vec![context_json.clone()], + }; - v => v.clone(), + let variant_condition = serde_json::json!({ + "==" : [ + { "var": "variant" }, + variant + ] + }); + conditions.push(variant_condition); + + let mut updated_ctx = Map::new(); + updated_ctx.insert(String::from("and"), serde_json::Value::Array(conditions)); + + match serde_json::to_value(updated_ctx) { + Ok(value) => Ok(value), + Err(e) => { + println!("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value {e}"); + Err("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value") + } } } diff --git a/crates/experimentation-platform/src/api/experiments/queries.rs b/crates/experimentation-platform/src/api/experiments/queries.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 526182b7f..af4c87633 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,6 +1,5 @@ use std::fmt; -use crate::db::models; use chrono::{DateTime, Utc}; use serde::{ de::{self, IntoDeserializer}, @@ -20,9 +19,13 @@ pub enum VariantType { pub struct Variant { pub id: String, pub variant_type: VariantType, + pub context_id: Option, + pub override_id: Option, pub overrides: Value, } +/********** Experiment Create Req Types ************/ + #[derive(Deserialize)] pub struct ExperimentCreateRequest { pub name: String, @@ -35,10 +38,35 @@ pub struct ExperimentCreateRequest { #[derive(Serialize)] pub struct ExperimentCreateResponse { - pub message: String, - pub data: models::Experiment, + pub experiment_id: i64, +} + +/********** Experiment Create Req Types END ************/ + +/********** Context Bulk API Type *************/ + +#[derive(Deserialize, Serialize, Clone)] +pub struct ContextPutReq { + pub context: serde_json::Map, + pub r#override: Value, } +#[derive(Deserialize, Serialize)] +pub enum ContextAction { + PUT(ContextPutReq), + DELETE(String), + MOVE((String, ContextPutReq)), +} + +#[derive(Deserialize, Serialize)] +pub struct ContextPutResp { + pub context_id: String, + pub override_id: String, + pub priority: i32, +} + +/********** Context Bulk API Type *************/ + #[derive(Deserialize, Debug)] pub struct ListFilters { #[serde(deserialize_with = "deserialize_stringified_list")] diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 6b08a6a5d..f9edee6af 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -21,6 +21,7 @@ pub struct ExperimentationFlags { } pub struct AppState { + pub cac_host: String, pub db_pool: Pool>, pub default_config_validation_schema: JSONSchema, pub admin_token: String, From e7e5eba254b8da9fbaaf87f5ed2ac11a196004e0 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Tue, 1 Aug 2023 12:56:59 +0530 Subject: [PATCH 065/352] feat: added conclude functionality for experiments --- .../src/api/experiments/handlers.rs | 117 +++++++++++++++++- .../src/api/experiments/types.rs | 9 ++ .../experimentation-platform/src/db/models.rs | 4 +- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index de389657d..0f7f6a5c1 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -1,7 +1,7 @@ use actix_web::{ get, http::StatusCode, - post, + patch, post, web::{self, Data, Json, Query}, Scope, }; @@ -16,8 +16,8 @@ use super::{ check_variants_override_coverage, validate_experiment, }, types::{ - ContextAction, ContextPutReq, ContextPutResp, ExperimentCreateRequest, - ExperimentCreateResponse, + ConcludeExperimentRequest, ContextAction, ContextPutReq, ContextPutResp, + ExperimentCreateRequest, ExperimentCreateResponse, Variant, }, }; use crate::{ @@ -28,6 +28,7 @@ use crate::{ pub fn endpoints() -> Scope { Scope::new("/experiments") .service(create) + .service(conclude) .service(list_experiments) } @@ -172,6 +173,116 @@ async fn create( } } +#[patch("/{experiment_id}/conclude")] +async fn conclude( + state: Data, + path: web::Path, + req: web::Json, + db_conn: DbConnection, + _auth_info: AuthenticationInfo +) -> actix_web::Result> { + use crate::db::schema::cac_v1::experiments::dsl; + + let experiment_id: i64 = path.into_inner(); + let winner_variant_id: String = req.into_inner().winner_variant.to_owned(); + + let DbConnection(mut conn) = db_conn; + let db_result = dsl::experiments + .find(experiment_id) + .get_result::(&mut conn); + + let experiment = match db_result { + Ok(response) => response, + Err(diesel::result::Error::NotFound) => { + return Err(actix_web::error::ErrorNotFound("experiment not found")); + } + Err(e) => { + println!("failed to fetch experiment from db: {e}"); + return Err(actix_web::error::ErrorInternalServerError( + "something went wrong.", + )); + } + }; + + if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { + return Err(actix_web::error::ErrorBadRequest( + "experiment is already concluded", + )); + } + + let experiment_context = experiment + .context + .as_object() + .ok_or(actix_web::error::ErrorInternalServerError(""))?; + + let mut operations: Vec = vec![]; + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|e| { + log::error!("parsing to variant type failed with err: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; + + let mut is_valid_winner_variant = false; + for variant in experiment_variants { + let context_id = variant + .context_id + .ok_or(actix_web::error::ErrorInternalServerError(""))?; + + if variant.id == winner_variant_id { + let context_put_req = ContextPutReq { + context: experiment_context.clone(), + r#override: variant.overrides, + }; + + is_valid_winner_variant = true; + operations.push(ContextAction::MOVE((context_id, context_put_req))); + } else { + // delete this context + operations.push(ContextAction::DELETE(context_id)); + } + } + + if !is_valid_winner_variant { + return Err(actix_web::error::ErrorNotFound("winner varaint not found")); + } + + // calling CAC bulk api with operations as payload + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + let response = http_client + .put(&url) + .bearer_auth(&state.admin_token) + .json(&operations) + .send() + .map_err(|e| { + println!("Failed to update contexts in CAC: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; + + if !response.status().is_success() { + return Err(actix_web::error::ErrorInternalServerError("")); + } + + // updating experiment status in db + let experiment_update_result = diesel::update(dsl::experiments) + .filter(dsl::id.eq(experiment_id)) + .set(( + dsl::status.eq(ExperimentStatusType::CONCLUDED), + dsl::last_modified.eq(Utc::now()), + )) + .get_result::(&mut conn); + + match experiment_update_result { + Ok(updated_experiment) => { + return Ok(Json(updated_experiment)); + } + Err(e) => { + println!("Failed to updated experiment status: {e}"); + return Err(actix_web::error::ErrorInternalServerError("")); + } + } +} + #[get("")] async fn list_experiments( state: Data, diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index af4c87633..d09a90a29 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -43,6 +43,15 @@ pub struct ExperimentCreateResponse { /********** Experiment Create Req Types END ************/ +/********** Experiment Conclude Req Types **********/ + +#[derive(Deserialize, Debug)] +pub struct ConcludeExperimentRequest { + pub winner_variant: String, +} + +/********** Experiment Conclude Req Types END **********/ + /********** Context Bulk API Type *************/ #[derive(Deserialize, Serialize, Clone)] diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index a17f840b3..f368fcd54 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -5,7 +5,9 @@ use diesel::{Insertable, Queryable, QueryableByName, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Clone, Copy, Deserialize, Serialize, diesel_derive_enum::DbEnum)] +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, diesel_derive_enum::DbEnum, +)] #[DbValueStyle = "UPPERCASE"] #[ExistingTypePath = "crate::db::schema::cac_v1::sql_types::ExperimentStatusType"] pub enum ExperimentStatusType { From d2000e6dd8bfd29edfd03d644bc71b1b1c21a699 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Wed, 2 Aug 2023 12:50:56 +0530 Subject: [PATCH 066/352] added GET experiment api --- .../src/api/experiments/handlers.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 0f7f6a5c1..859064416 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -30,6 +30,7 @@ pub fn endpoints() -> Scope { .service(create) .service(conclude) .service(list_experiments) + .service(get_experiment) } #[post("")] @@ -333,3 +334,31 @@ async fn list_experiments( } }; } + +#[get("/{id}")] +async fn get_experiment( + params: web::Path, + db_conn: DbConnection, +) -> actix_web::Result> { + use crate::db::schema::cac_v1::experiments::dsl::*; + + let experiment_id = params.into_inner(); + let DbConnection(mut conn) = db_conn; + + let db_result = experiments.find(experiment_id).get_result(&mut conn); + + let response = match db_result { + Ok(result) => result, + Err(diesel::result::Error::NotFound) => { + return Err(actix_web::error::ErrorNotFound( + "Experiment not found".to_string(), + )); + } + Err(e) => { + log::error!("{}", format!("get experiments failed due to : {e:?}")); + return Err(actix_web::error::ErrorInternalServerError("")); + } + }; + + return Ok(Json(response)); +} From 81e63c0b8eb19d7b03783c96d79137d40eb86e5a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Wed, 26 Jul 2023 17:04:01 +0530 Subject: [PATCH 067/352] -Ramp Api --- .../src/api/experiments/handlers.rs | 64 ++++++++++++++++++- .../src/api/experiments/types.rs | 5 ++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 859064416..2a21bcc72 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -17,7 +17,7 @@ use super::{ }, types::{ ConcludeExperimentRequest, ContextAction, ContextPutReq, ContextPutResp, - ExperimentCreateRequest, ExperimentCreateResponse, Variant, + ExperimentCreateRequest, ExperimentCreateResponse, Variant, RampRequest, }, }; use crate::{ @@ -31,6 +31,7 @@ pub fn endpoints() -> Scope { .service(conclude) .service(list_experiments) .service(get_experiment) + .service(ramp) } #[post("")] @@ -362,3 +363,64 @@ async fn get_experiment( return Ok(Json(response)); } + +#[patch("/{id}/ramp")] +async fn ramp( + params: web::Path, + req: web::Json, + db_conn: DbConnection, +) -> actix_web::Result> { + let DbConnection(mut conn) = db_conn; + let exp_id = params.into_inner(); + + use crate::db::schema::cac_v1::experiments::dsl::*; + let db_result: Result = experiments + .find(exp_id) + .get_result::(&mut conn); + + let experiment = match db_result { + Ok(result) => result, + Err(diesel::result::Error::NotFound) => + return Err(actix_web::error::ErrorNotFound("No results found")), + Err(e) => { + println!("{e}"); + return Err(actix_web::error::ErrorInternalServerError("Something went wrong")); + } + }; + + let old_traffic_percentage = experiment.traffic_percentage as u8; + let new_traffic_percentage = req.traffic_percentage as u8; + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|e| { + log::error!("parsing to variant type failed with err: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; + let variants_count = experiment_variants.len() as u8; + let max = 100 / variants_count; + + if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { + return Err(actix_web::error::ErrorBadRequest("Experiment is already concluded")); + } else if new_traffic_percentage > max { + println!("The Traffic percentage provided exceeds the range"); + return Err(actix_web::error::ErrorBadRequest( + format!("The traffic_percentage cannot exceed {}", max), + )); + } else if new_traffic_percentage == old_traffic_percentage { + return Err(actix_web::error::ErrorBadRequest("The traffic_percentage is same as provided")); + } + + let update = diesel::update(experiments) + .filter(id.eq(exp_id)) + .set(traffic_percentage.eq(req.traffic_percentage as i32)) + .execute(&mut conn); + + match update { + Ok(0) => return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")), + Ok(_) => return Ok(Json(format!("Traffic percentage has been updated for the experiment id : {}", exp_id))), + Err(e) => { + println!("Failed to update the traffic_percentage: {e}"); + return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")); + } + } +} + diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index d09a90a29..47fccc310 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -120,3 +120,8 @@ where deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) } + +#[derive(Deserialize,Debug)] +pub struct RampRequest { + pub traffic_percentage: u64, +} From 2d813f4231ffd6eefff2496d24de73d552d78411 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Tue, 1 Aug 2023 14:04:35 +0530 Subject: [PATCH 068/352] Migrated all logs to consume single function for JSON formatting --- Cargo.lock | 1 + .../src/api/context/handlers.rs | 15 +++---- .../src/api/default_config/handlers.rs | 8 ++-- .../src/api/dimension/handlers.rs | 4 +- crates/context-aware-config/src/logger.rs | 44 +++++++++++++++++++ crates/context-aware-config/src/main.rs | 4 +- .../src/api/experiments/handlers.rs | 26 +++++------ .../src/api/experiments/helpers.rs | 6 +-- crates/service-utils/Cargo.toml | 1 + crates/service-utils/src/helpers.rs | 6 +-- crates/service-utils/src/service/types.rs | 11 ++--- libs/rust/src/lib.rs | 4 +- 12 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 crates/context-aware-config/src/logger.rs diff --git a/Cargo.lock b/Cargo.lock index d25fdadc8..344948596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2934,6 +2934,7 @@ dependencies = [ "diesel", "dotenv", "jsonschema", + "log", "rs-snowflake", "rusoto_core", "rusoto_kms", diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 9d13558b7..4f8ecfdc0 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -6,7 +6,7 @@ use crate::{ contexts::{self, id}, dimensions::dsl::dimensions, }, - }, + } }; use actix_web::{ delete, @@ -22,7 +22,6 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use log::info; use serde_json::{json, Value, Value::Null}; use service_utils::{ helpers::ToActixErr, @@ -175,7 +174,7 @@ async fn put_handler( put(req, &auth_info, conn) .map(|resp| web::Json(resp)) .map_err(|e| { - println!("context put failed with error: {:?}", e); + log::info!("context put failed with error: {:?}", e); ErrorInternalServerError("") }) } @@ -245,7 +244,7 @@ async fn move_handler( r#move(path.into_inner(), req, &auth_info, conn, true) .map(|resp| web::Json(resp)) .map_err(|e| { - println!("move api failed with error: {:?}", e); + log::info!("move api failed with error: {:?}", e); ErrorInternalServerError("") }) } @@ -261,7 +260,7 @@ async fn get_context( let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { - println!("Unable to get db connection from pool, error: {e}"); + log::info!("Unable to get db connection from pool, error: {e}"); return Err(error::ErrorInternalServerError("")); } }; @@ -272,7 +271,7 @@ async fn get_context( let ctx_vec = match result { Ok(ctx_vec) => ctx_vec, Err(e) => { - println!("Failed to execute query, error: {e}"); + log::info!("Failed to execute query, error: {e}"); return Err(error::ErrorInternalServerError("")); } }; @@ -337,7 +336,7 @@ async fn delete_context( match deleted_row { Ok(0) => Err(ErrorNotFound("")), Ok(_) => { - info!("{ctx_id} context deleted by {email}"); + log::info!("{ctx_id} context deleted by {email}"); Ok(HttpResponse::NoContent().finish()) } Err(e) => { @@ -391,7 +390,7 @@ async fn bulk_operations( match deleted_row { Ok(0) => return Err(diesel::result::Error::RollbackTransaction), Ok(_) => { - info!("{ctx_id} context deleted by {email}"); + log::info!("{ctx_id} context deleted by {email}"); resp.push(json!(format!("{ctx_id} deleted succesfully"))) } Err(e) => { diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 20eaccad5..6919da4fa 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -38,7 +38,7 @@ async fn create( let jschema = match schema_compile_result { Ok(jschema) => jschema, Err(e) => { - println!("Failed to compile as a Draft-7 JSON schema: {e}"); + log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); return HttpResponse::BadRequest().body("Bad json schema."); } }; @@ -46,7 +46,7 @@ async fn create( match jschema.validate(&req.value) { Ok(_) => (), Err(_) => { - println!("Validation for value with given JSON schema failed."); + log::info!("Validation for value with given JSON schema failed."); return HttpResponse::BadRequest() .body("Validation with given schema failed."); } @@ -65,7 +65,7 @@ async fn create( let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { - println!("unable to get db connection from pool, error: {e}"); + log::info!("unable to get db connection from pool, error: {e}"); return HttpResponse::InternalServerError().finish(); } }; @@ -79,7 +79,7 @@ async fn create( return HttpResponse::Created().body("DefaultConfig created successfully.") } Err(e) => { - println!("DefaultConfig creation failed with error: {e}"); + log::info!("DefaultConfig creation failed with error: {e}"); return HttpResponse::InternalServerError() .body("Failed to create DefaultConfig"); } diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 571f1d116..d6750bcd2 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -39,7 +39,7 @@ async fn create( let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { - println!("unable to get db connection from pool, error: {e}"); + log::info!("unable to get db connection from pool, error: {e}"); return HttpResponse::InternalServerError().finish(); } }; @@ -57,7 +57,7 @@ async fn create( .body("Dimension created/updated successfully.") } Err(e) => { - println!("Dimension upsert failed with error: {e}"); + log::info!("Dimension upsert failed with error: {e}"); return HttpResponse::InternalServerError() .body("Failed to create/udpate dimension\n"); } diff --git a/crates/context-aware-config/src/logger.rs b/crates/context-aware-config/src/logger.rs new file mode 100644 index 000000000..275cc2818 --- /dev/null +++ b/crates/context-aware-config/src/logger.rs @@ -0,0 +1,44 @@ +use env_logger::{Builder, Env}; +use serde::{ser::SerializeStruct, Serialize, Serializer}; +use serde_json::{json, Value}; +use std::io::Write; + +//TODO make Log type more strict and have custom implementation of +//serialize trait +pub struct Log { + value: Value, + timestamp: String, + level: String, +} + +//NOTE we can't use '-' in a struct identifier name therefore writing custom serializer +impl Serialize for Log { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let no_of_fields = 4; + let mut log = serializer.serialize_struct("Log", no_of_fields)?; + log.serialize_field("value", &self.value)?; + log.serialize_field("service", "context-aware-config")?; + log.serialize_field("timestamp", &self.timestamp)?; + log.serialize_field("level", &self.level)?; + log.end() + } +} + +pub fn init_logger() { + let env = Env::default(); + Builder::from_env(env) + .format(move |buf, record| { + let log = Log { + timestamp: buf.timestamp_millis().to_string(), + value: json!(record.args()), + level: record.level().to_string(), + }; + + writeln!(buf, "{}", serde_json::json!(log)) + }) + .target(env_logger::Target::Stdout) + .init(); +} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 5186c0c44..8c4798ebb 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -1,8 +1,10 @@ mod api; mod db; mod helpers; +mod logger; use dotenv; +use logger::init_logger; use std::{env, io::Result}; use actix_web::{ @@ -25,7 +27,7 @@ use experimentation_platform::api::*; #[actix_web::main] async fn main() -> Result<()> { dotenv::dotenv().ok(); - env_logger::init(); + init_logger(); let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 2a21bcc72..180a3bc2d 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -121,12 +121,12 @@ async fn create( .json(&cac_operations) .send() .map_err(|e| { - println!("failed to create contexts in cac: {e}"); + log::info!("failed to create contexts in cac: {e}"); actix_web::error::ErrorInternalServerError("") })? .json::>() .map_err(|e| { - println!("failed to parse response: {e}"); + log::info!("failed to parse response: {e}"); actix_web::error::ErrorInternalServerError("") })?; @@ -167,7 +167,7 @@ async fn create( return Ok(Json(response)); } Err(e) => { - println!("Experiment creation failed with error: {e}"); + log::info!("Experiment creation failed with error: {e}"); return Err(actix_web::error::ErrorInternalServerError( "Failed to create experiment".to_string(), )); @@ -199,7 +199,7 @@ async fn conclude( return Err(actix_web::error::ErrorNotFound("experiment not found")); } Err(e) => { - println!("failed to fetch experiment from db: {e}"); + log::info!("failed to fetch experiment from db: {e}"); return Err(actix_web::error::ErrorInternalServerError( "something went wrong.", )); @@ -257,7 +257,7 @@ async fn conclude( .json(&operations) .send() .map_err(|e| { - println!("Failed to update contexts in CAC: {e}"); + log::info!("Failed to update contexts in CAC: {e}"); actix_web::error::ErrorInternalServerError("") })?; @@ -279,7 +279,7 @@ async fn conclude( return Ok(Json(updated_experiment)); } Err(e) => { - println!("Failed to updated experiment status: {e}"); + log::info!("Failed to updated experiment status: {e}"); return Err(actix_web::error::ErrorInternalServerError("")); } } @@ -293,7 +293,7 @@ async fn list_experiments( let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { - println!("Unable to get db connection from pool, error: {e}"); + log::info!("Unable to get db connection from pool, error: {e}"); return Err(AppError { message: "Could not connect to the database".to_string(), possible_fix: "Try after sometime".to_string(), @@ -311,7 +311,7 @@ async fn list_experiments( .limit(filters.count) .offset((filters.page - 1) * filters.count); - // println!( + // log::info!( // "List filter query: {:?}", // diesel::debug_query::(&query) // ); @@ -380,10 +380,10 @@ async fn ramp( let experiment = match db_result { Ok(result) => result, - Err(diesel::result::Error::NotFound) => + Err(diesel::result::Error::NotFound) => return Err(actix_web::error::ErrorNotFound("No results found")), Err(e) => { - println!("{e}"); + log::info!("{e}"); return Err(actix_web::error::ErrorInternalServerError("Something went wrong")); } }; @@ -401,7 +401,7 @@ async fn ramp( if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { return Err(actix_web::error::ErrorBadRequest("Experiment is already concluded")); } else if new_traffic_percentage > max { - println!("The Traffic percentage provided exceeds the range"); + log::info!("The Traffic percentage provided exceeds the range"); return Err(actix_web::error::ErrorBadRequest( format!("The traffic_percentage cannot exceed {}", max), )); @@ -413,12 +413,12 @@ async fn ramp( .filter(id.eq(exp_id)) .set(traffic_percentage.eq(req.traffic_percentage as i32)) .execute(&mut conn); - + match update { Ok(0) => return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")), Ok(_) => return Ok(Json(format!("Traffic percentage has been updated for the experiment id : {}", exp_id))), Err(e) => { - println!("Failed to update the traffic_percentage: {e}"); + log::info!("Failed to update the traffic_percentage: {e}"); return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")); } } diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 5d33f1c8e..f58e63365 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -154,7 +154,7 @@ pub fn validate_experiment( let active_experiments: Vec = active_experiments_filter.load(conn).map_err(|e| { - println!("validate_experiment: {e}"); + log::info!("validate_experiment: {e}"); "Failed to fetch active experiments" })?; @@ -168,7 +168,7 @@ pub fn validate_experiment( let are_overlapping = are_overlapping_contexts(&experiment.context, &active_experiment.context) .map_err(|e| { - println!("validate_experiment: {e}"); + log::info!("validate_experiment: {e}"); "Failed to validate for overlapping context" })?; @@ -229,7 +229,7 @@ pub fn add_variant_dimension_to_ctx( match serde_json::to_value(updated_ctx) { Ok(value) => Ok(value), Err(e) => { - println!("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value {e}"); + log::info!("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value {e}"); Err("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value") } } diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index b0b00bb33..edc954b02 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -22,3 +22,4 @@ rusoto_core = "0.48.0" base64 = "0.21.2" urlencoding = "~2.1.2" jsonschema = "~0.17" +log = "^0.4" diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index a113fd8f7..6b3ed7f57 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -12,7 +12,7 @@ where std::env::var(name) .map(|val| val.parse().unwrap()) .map_err(|e| { - println!("{name} env not found with error: {e}"); + log::info!("{name} env not found with error: {e}"); return e; }) } @@ -40,11 +40,11 @@ where B: fmt::Debug + fmt::Display + 'static, { self.map_err(|e| { - println!("{log_prefix}, err: {e:?}"); + log::info!("{log_prefix}, err: {e:?}"); ErrorInternalServerError(err_body) }) } } -// pub fn db_connection_from_state(state: T) -> +// pub fn db_connection_from_state(state: T) -> diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index f9edee6af..b0a1b4819 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -4,10 +4,7 @@ use diesel::{ }; use jsonschema::JSONSchema; -use std::{ - future::{ready, Ready}, - println, -}; +use std::future::{ready, Ready}; use actix_web::{error, web::Data, Error, FromRequest}; @@ -63,7 +60,7 @@ impl FromRequest for AuthenticationInfo { let result = match (opt_token, opt_admin_token) { (_, None) => { - println!("ERROR: ADMIN TOKEN NOT FOUND!!!!"); + log::info!("ERROR: ADMIN TOKEN NOT FOUND!!!!"); Err(error::ErrorInternalServerError("")) } (None, _) => Err(error::ErrorUnauthorized("Bearer token required.")), @@ -92,14 +89,14 @@ impl FromRequest for DbConnection { let app_state = match req.app_data::>() { Some(state) => state, None => { - println!("Unable to get app_data from request"); + log::info!("Unable to get app_data from request"); return ready(Err(error::ErrorInternalServerError(""))); } }; let result = match app_state.db_pool.get() { Ok(conn) => Ok(DbConnection(conn)), Err(e) => { - println!("Unable to get db connection from pool, error: {e}"); + log::info!("Unable to get db connection from pool, error: {e}"); Err(error::ErrorInternalServerError("")) } }; diff --git a/libs/rust/src/lib.rs b/libs/rust/src/lib.rs index 1e2fd8796..a623f57dd 100644 --- a/libs/rust/src/lib.rs +++ b/libs/rust/src/lib.rs @@ -75,7 +75,7 @@ impl Client { StatusCode::NOT_MODIFIED => { return Err(String::from("CAC: skipping update, remote not modified")); } - StatusCode::OK => println!("CAC: new config received, updating"), + StatusCode::OK => log::info!("CAC: new config received, updating"), x => return Err(format!("CAC: fetch failed, status: {}", x,)), }; resp.json::().await.map_err_to_string() @@ -99,7 +99,7 @@ impl Client { .update_cac() .await .unwrap_or_else(identity); - println!("{result}",); + log::info!("{result}",); } }); } From 9ed8a884443f83ed65a70bd803a7ccd1b5838445 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Fri, 4 Aug 2023 08:54:20 +0530 Subject: [PATCH 069/352] fix: updated last_modified in ramp --- .../experimentation-platform/src/api/experiments/handlers.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 180a3bc2d..424626dd4 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -411,7 +411,10 @@ async fn ramp( let update = diesel::update(experiments) .filter(id.eq(exp_id)) - .set(traffic_percentage.eq(req.traffic_percentage as i32)) + .set(( + traffic_percentage.eq(req.traffic_percentage as i32), + last_modified.eq(Utc::now()), + )) .execute(&mut conn); match update { From cac419c6eebc9bc1a6f3ee1e3cd0f472c06a9b66 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Wed, 2 Aug 2023 17:59:45 +0530 Subject: [PATCH 070/352] ci: switch to using newman --- package-lock.json | 42 ++++++++++---------------------------- package.json | 4 ++-- postman/cac/.variable.json | 2 +- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7ae828c6..9d4b76479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "context-aware-configuration", "version": "0.0.1", "devDependencies": { - "newmandir": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git" + "newmandir": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" } }, "node_modules/@postman/form-data": { @@ -319,12 +319,12 @@ } }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "engines": { - "node": ">=14" + "node": ">= 10" } }, "node_modules/core-util-is": { @@ -781,11 +781,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/newman": { + "node_modules/newmandir": { + "name": "newman", "version": "5.3.2", - "resolved": "https://registry.npmjs.org/newman/-/newman-5.3.2.tgz", - "integrity": "sha512-cWy8pV0iwvMOZLTw3hkAHcwo2ZA0GKkXm8oUMn1Ltii3ZI2nKpnrg9QGdIT0hGHChRkX6prY5e3Aar7uykMGNg==", + "resolved": "git+ssh://git@github.com/knutties/newman.git#5a27c2ea3e80c21d8bfabdebbdec48e8ed5d69fd", "dev": true, + "license": "Apache-2.0", "dependencies": { "async": "3.2.3", "chardet": "1.4.0", @@ -794,8 +795,10 @@ "colors": "1.4.0", "commander": "7.2.0", "csv-parse": "4.16.3", + "directory-tree": "3.5.1", "eventemitter3": "4.0.7", "filesize": "8.0.7", + "liquid-json": "0.3.1", "lodash": "4.17.21", "mkdirp": "1.0.4", "postman-collection": "4.1.1", @@ -816,29 +819,6 @@ "node": ">=10" } }, - "node_modules/newman/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/newmandir": { - "version": "0.0.1", - "resolved": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git#f9b700676e72c601e60793a8a0e083c5b06800cf", - "dev": true, - "license": "ISC", - "dependencies": { - "commander": "^10.0.1", - "directory-tree": "^3.5.1", - "newman": "^5.3.2" - }, - "bin": { - "newmandir": "index.js" - } - }, "node_modules/node-oauth1": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/node-oauth1/-/node-oauth1-1.3.0.tgz", diff --git a/package.json b/package.json index 99a1126c7..2802d40a3 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "private": true, "description": "This is just to run automated newman tests for this service", "scripts": { - "test": "./node_modules/.bin/newmandir -n postman/cac" + "test": "./node_modules/.bin/newman dir-run postman/cac" }, "devDependencies": { - "newmandir": "git+ssh://git@ssh.bitbucket.juspay.net/~natarajan_juspay.in/newmandir.git" + "newmandir": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" } } diff --git a/postman/cac/.variable.json b/postman/cac/.variable.json index 5460e39fb..191e8214a 100644 --- a/postman/cac/.variable.json +++ b/postman/cac/.variable.json @@ -2,7 +2,7 @@ "variable": [ { "key": "host", - "value": "http://localhost:8080", + "value": "http://127.0.0.1:8080", "type": "default" }, { From 28443cc0cccf6ffac4e42efaa17e8e49144de1a0 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Fri, 4 Aug 2023 14:43:37 +0530 Subject: [PATCH 071/352] fixed transactions in bulk api --- .../src/api/context/handlers.rs | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 4f8ecfdc0..85a20093f 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -6,7 +6,7 @@ use crate::{ contexts::{self, id}, dimensions::dsl::dimensions, }, - } + }, }; use actix_web::{ delete, @@ -140,6 +140,7 @@ fn put( req: web::Json, auth_info: &AuthenticationInfo, conn: &mut PooledConnection>, + already_under_txn: bool, ) -> Result { use contexts::dsl::contexts; @@ -148,10 +149,21 @@ fn put( Error::STRTYPE(e.to_string()) })?; + if already_under_txn { + diesel::sql_query("SAVEPOINT put_ctx_savepoint") + .execute(conn) + .map_err(|e| Error::DIESEL(e))?; + } let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); + match insert { Ok(_) => Ok(get_put_resp(new_ctx)), Err(DatabaseError(UniqueViolation, _)) => { + if already_under_txn { + diesel::sql_query("ROLLBACK TO put_ctx_savepoint") + .execute(conn) + .map_err(|e| Error::DIESEL(e))?; + } update_override_of_existing_ctx(conn, new_ctx).map_err(|e| Error::DIESEL(e)) } Err(e) => { @@ -171,7 +183,7 @@ async fn put_handler( .db_pool .get() .map_err_to_internal_server("unable to get db connection from pool", "")?; - put(req, &auth_info, conn) + put(req, &auth_info, conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("context put failed with error: {:?}", e); @@ -184,32 +196,39 @@ fn r#move( req: web::Json, auth_info: &AuthenticationInfo, conn: &mut PooledConnection>, - with_transaction: bool, + already_under_txn: bool, ) -> Result { use contexts::dsl; let new_ctx = create_ctx_from_put_req(req, conn, auth_info).map_err(|e| { log::error!("update query failed with error: {e:?}"); Error::STRTYPE(e.to_string()) })?; + + if already_under_txn { + diesel::sql_query("SAVEPOINT update_ctx_savepoint") + .execute(conn) + .map_err(|e| Error::DIESEL(e))?; + } + let update = diesel::update(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) .set((&new_ctx, dsl::id.eq(&new_ctx.id))) .execute(conn); let handle_unique_violation = - |db_conn: &mut DBConnection, new_ctx: Context, with_transaction: bool| { - if with_transaction { + |db_conn: &mut DBConnection, new_ctx: Context, already_under_txn: bool| { + if already_under_txn { + diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .execute(db_conn)?; + update_override_of_existing_ctx(db_conn, new_ctx) + } else { db_conn.build_transaction().read_write().run(|conn| { diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) .execute(conn)?; update_override_of_existing_ctx(conn, new_ctx) }) - } else { - diesel::delete(dsl::contexts) - .filter(dsl::id.eq(&old_ctx_id)) - .execute(db_conn)?; - update_override_of_existing_ctx(db_conn, new_ctx) } }; @@ -219,7 +238,12 @@ fn r#move( ))), Ok(_) => Ok(get_put_resp(new_ctx)), Err(DatabaseError(UniqueViolation, _)) => { - handle_unique_violation(conn, new_ctx, with_transaction) + if already_under_txn { + diesel::sql_query("ROLLBACK TO update_ctx_savepoint") + .execute(conn) + .map_err(|e| Error::DIESEL(e))?; + } + handle_unique_violation(conn, new_ctx, already_under_txn) .map_err(|e| Error::DIESEL(e)) } Err(e) => { @@ -241,7 +265,7 @@ async fn move_handler( .get() .map_err_to_internal_server("unable to get db connection from pool", "")?; - r#move(path.into_inner(), req, &auth_info, conn, true) + r#move(path.into_inner(), req, &auth_info, conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("move api failed with error: {:?}", e); @@ -370,8 +394,12 @@ async fn bulk_operations( for action in reqs.into_inner().into_iter() { match action { ContextAction::PUT(put_req) => { - let resp_result = - put(actix_web::web::Json(put_req), &auth_info, transaction_conn); + let resp_result = put( + actix_web::web::Json(put_req), + &auth_info, + transaction_conn, + true, + ); match resp_result { Ok(put_resp) => { @@ -405,7 +433,7 @@ async fn bulk_operations( actix_web::web::Json(put_req), &auth_info, transaction_conn, - false, + true, ); match move_context_resp { From be0b70fa2135d47f32726ec02eddb5ec37e62ba1 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 26 Jul 2023 12:36:41 +0530 Subject: [PATCH 072/352] feat: added experimentation client with few fixes --- .gitignore | 48 +- Cargo.lock | 149 +- Cargo.toml | 3 + {libs/rust => crates/cac_client}/Cargo.toml | 1 + {libs/rust => crates/cac_client}/src/eval.rs | 0 {libs/rust => crates/cac_client}/src/lib.rs | 0 .../cac_client}/src/utils/core.rs | 0 .../cac_client}/src/utils/deep_merge.rs | 0 .../cac_client}/src/utils/mod.rs | 0 crates/context-aware-config/src/db/schema.rs | 1 + .../2023-08-02-071224_add-index/down.sql | 5 + .../2023-08-02-071224_add-index/up.sql | 5 + .../src/api/experiments/handlers.rs | 23 +- .../experimentation-platform/src/db/models.rs | 2 +- .../experimentation-platform/src/db/schema.rs | 2 +- crates/superposition_client/Cargo.toml | 13 + crates/superposition_client/README.md | 0 crates/superposition_client/src/lib.rs | 140 ++ crates/superposition_client/src/types.rs | 38 + .../Cargo.toml | 15 + .../src/main.rs | 46 + docker-compose.yaml | 20 - flake.nix | 5 + libs/rust/Cargo.lock | 1811 ----------------- makefile | 37 +- 25 files changed, 414 insertions(+), 1950 deletions(-) rename {libs/rust => crates/cac_client}/Cargo.toml (96%) rename {libs/rust => crates/cac_client}/src/eval.rs (100%) rename {libs/rust => crates/cac_client}/src/lib.rs (100%) rename {libs/rust => crates/cac_client}/src/utils/core.rs (100%) rename {libs/rust => crates/cac_client}/src/utils/deep_merge.rs (100%) rename {libs/rust => crates/cac_client}/src/utils/mod.rs (100%) create mode 100644 crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql create mode 100644 crates/superposition_client/Cargo.toml create mode 100644 crates/superposition_client/README.md create mode 100644 crates/superposition_client/src/lib.rs create mode 100644 crates/superposition_client/src/types.rs create mode 100644 crates/superposition_client_integration_example/Cargo.toml create mode 100644 crates/superposition_client_integration_example/src/main.rs delete mode 100644 libs/rust/Cargo.lock diff --git a/.gitignore b/.gitignore index 3dfc43b9c..d0f7e44f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,12 @@ -# These are some examples of commonly ignored file patterns. -# You should customize this list as applicable to your project. -# Learn more about .gitignore: -# https://www.atlassian.com/git/tutorials/saving-changes/gitignore - -# Node artifact files -node_modules/ -dist/ -build/ - -/backend/target - -# Compiled Java class files -*.class - -# Compiled Python bytecode -*.py[cod] - # Log files *.log -# Package files -*.jar - -# Maven +# cargo target/ -dist/ - -# JetBrains IDE -.idea/ - -# Unit test reports -TEST*.xml # Generated by MacOS .DS_Store -# Generated by Windows -Thumbs.db - -# Applications -*.app -*.exe -*.war - -# Large media files -*.mp4 -*.tiff -*.avi -*.flv -*.mov -*.wmv - .env # nix build outputs @@ -65,6 +21,6 @@ backend/.env bacon.toml docker-compose/localstack/export_cyphers.sh test_logs - .keep .vscode +*.session.sql diff --git a/Cargo.lock b/Cargo.lock index 344948596..e07f5b1f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,7 +22,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "smallvec 1.10.0", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-util", ] @@ -39,7 +39,7 @@ dependencies = [ "log", "memchr", "pin-project-lite", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-util", ] @@ -76,7 +76,7 @@ dependencies = [ "rand 0.8.5", "sha1", "smallvec 1.10.0", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-util", "tracing", "zstd", @@ -112,7 +112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" dependencies = [ "futures-core", - "tokio 1.28.1", + "tokio 1.29.1", ] [[package]] @@ -129,7 +129,7 @@ dependencies = [ "mio 0.8.6", "num_cpus", "socket2", - "tokio 1.28.1", + "tokio 1.29.1", "tracing", ] @@ -281,6 +281,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -365,7 +371,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -554,6 +560,19 @@ dependencies = [ "bytes 1.4.0", ] +[[package]] +name = "cac" +version = "0.1.0" +dependencies = [ + "actix-web", + "chrono", + "jsonlogic", + "log", + "reqwest 0.11.18", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.0.79" @@ -577,13 +596,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", "time 0.1.45", @@ -624,7 +643,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -887,7 +906,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -904,7 +923,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -958,7 +977,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -967,7 +986,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -1072,6 +1091,17 @@ dependencies = [ "libc", ] +[[package]] +name = "example" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "serde_json", + "superposition_client", +] + [[package]] name = "experimentation-platform" version = "0.1.0" @@ -1280,7 +1310,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -1374,7 +1404,7 @@ dependencies = [ "http 0.2.9", "indexmap", "slab", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-util", "tracing", ] @@ -1542,7 +1572,7 @@ dependencies = [ "itoa 1.0.6", "pin-project-lite", "socket2", - "tokio 1.28.1", + "tokio 1.29.1", "tower-service", "tracing", "want 0.3.0", @@ -1557,7 +1587,7 @@ dependencies = [ "http 0.2.9", "hyper 0.14.26", "rustls", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-rustls", ] @@ -1583,7 +1613,7 @@ dependencies = [ "bytes 1.4.0", "hyper 0.14.26", "native-tls", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-native-tls", ] @@ -1751,6 +1781,15 @@ dependencies = [ "treediff", ] +[[package]] +name = "jsonlogic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0699c6109316a5add3d42ae21ae954f207739f12b68914a0d447b9aa45e6f" +dependencies = [ + "serde_json", +] + [[package]] name = "jsonschema" version = "0.17.0" @@ -1805,9 +1844,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "link-cplusplus" @@ -2166,7 +2205,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -2289,9 +2328,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -2308,9 +2347,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2587,7 +2626,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded 0.7.1", - "tokio 1.28.1", + "tokio 1.29.1", "tokio-native-tls", "tokio-rustls", "tower-service", @@ -2641,7 +2680,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "serde_json", - "tokio 1.28.1", + "tokio 1.29.1", "xml-rs", ] @@ -2659,7 +2698,7 @@ dependencies = [ "serde", "serde_json", "shlex", - "tokio 1.28.1", + "tokio 1.29.1", "zeroize", ] @@ -2700,7 +2739,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "sha2", - "tokio 1.28.1", + "tokio 1.29.1", ] [[package]] @@ -2870,29 +2909,29 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.162" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.162" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa 1.0.6", "ryu", @@ -3061,6 +3100,19 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "superposition_client" +version = "0.1.0" +dependencies = [ + "chrono", + "dotenv", + "jsonlogic", + "reqwest 0.11.18", + "serde", + "serde_json", + "tokio 1.29.1", +] + [[package]] name = "syn" version = "1.0.109" @@ -3074,9 +3126,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -3135,7 +3187,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -3212,11 +3264,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.28.1" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg 1.1.0", + "backtrace", "bytes 1.4.0", "libc", "mio 0.8.6", @@ -3279,7 +3332,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -3289,7 +3342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.28.1", + "tokio 1.29.1", ] [[package]] @@ -3318,7 +3371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", - "tokio 1.28.1", + "tokio 1.29.1", ] [[package]] @@ -3384,7 +3437,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", - "tokio 1.28.1", + "tokio 1.29.1", "tracing", ] @@ -3618,7 +3671,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -3652,7 +3705,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 68417c118..8d23441ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,7 @@ members = [ "crates/service-utils", "crates/context-aware-config", + "crates/superposition_client", + "crates/cac_client", + "crates/superposition_client_integration_example" ] diff --git a/libs/rust/Cargo.toml b/crates/cac_client/Cargo.toml similarity index 96% rename from libs/rust/Cargo.toml rename to crates/cac_client/Cargo.toml index 7cde04bca..979017c64 100644 --- a/libs/rust/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -12,3 +12,4 @@ jsonlogic = "0.5.1" reqwest = { version = "0.11.18", features = ["json"]} serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" +log = "^0.4" diff --git a/libs/rust/src/eval.rs b/crates/cac_client/src/eval.rs similarity index 100% rename from libs/rust/src/eval.rs rename to crates/cac_client/src/eval.rs diff --git a/libs/rust/src/lib.rs b/crates/cac_client/src/lib.rs similarity index 100% rename from libs/rust/src/lib.rs rename to crates/cac_client/src/lib.rs diff --git a/libs/rust/src/utils/core.rs b/crates/cac_client/src/utils/core.rs similarity index 100% rename from libs/rust/src/utils/core.rs rename to crates/cac_client/src/utils/core.rs diff --git a/libs/rust/src/utils/deep_merge.rs b/crates/cac_client/src/utils/deep_merge.rs similarity index 100% rename from libs/rust/src/utils/deep_merge.rs rename to crates/cac_client/src/utils/deep_merge.rs diff --git a/libs/rust/src/utils/mod.rs b/crates/cac_client/src/utils/mod.rs similarity index 100% rename from libs/rust/src/utils/mod.rs rename to crates/cac_client/src/utils/mod.rs diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 992f759d9..12081af33 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -44,6 +44,7 @@ pub mod cac_v1 { } } + diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, diff --git a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql new file mode 100644 index 000000000..395b03b1c --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE cac_v1.experiments +ALTER COLUMN last_modified DROP NOT NULL, +ALTER COLUMN last_modified DROP DEFAULT; +DROP INDEX experiment_status_index; diff --git a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql new file mode 100644 index 000000000..b2d158090 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE cac_v1.experiments +ALTER COLUMN last_modified SET NOT NULL, +ALTER COLUMN last_modified SET DEFAULT NOW(); +CREATE INDEX experiment_status_index ON cac_v1.experiments (status) INCLUDE (created_at, last_modified); diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 424626dd4..cbebe017d 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -68,7 +68,13 @@ async fn create( } //traffic_percentage should be max 100/length of variants - // TODO: Add traffic_percentage validation + let allowed_traffic = (100 / variants.len()) as i32; + if req.traffic_percentage > allowed_traffic { + return Err(actix_web::error::ErrorBadRequest(format!( + "the traffic percentage specified ({}) is greater than what can be evenly distributed among variants. Reduce the traffic percentage to a value <= {}", + req.traffic_percentage, allowed_traffic + ))); + } // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; @@ -144,7 +150,7 @@ async fn create( id: experiment_id, created_by: email, created_at: Utc::now(), - last_modified: Option::None, + last_modified: Utc::now(), name: req.name.to_string(), override_keys: req.override_keys.to_vec(), traffic_percentage: req.traffic_percentage, @@ -295,8 +301,8 @@ async fn list_experiments( Err(e) => { log::info!("Unable to get db connection from pool, error: {e}"); return Err(AppError { - message: "Could not connect to the database".to_string(), - possible_fix: "Try after sometime".to_string(), + message: "Something went wrong".to_string(), + possible_fix: "Try again after some time".to_string(), status_code: StatusCode::INTERNAL_SERVER_ERROR, }); // return an error @@ -306,15 +312,12 @@ async fn list_experiments( use crate::db::schema::cac_v1::experiments::dsl::*; let query = experiments .filter(status.eq_any(filters.status.clone())) - .filter(created_at.ge(filters.from_date)) - .filter(created_at.le(filters.to_date)) + .filter(last_modified.ge(filters.from_date)) + .filter(last_modified.le(filters.to_date)) + .order(last_modified.desc()) .limit(filters.count) .offset((filters.page - 1) * filters.count); - // log::info!( - // "List filter query: {:?}", - // diesel::debug_query::(&query) - // ); let db_result = query.load::(&mut conn); match db_result { diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index f368fcd54..c4ec8dd6c 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -23,7 +23,7 @@ pub struct Experiment { pub id: i64, pub created_at: DateTime, pub created_by: String, - pub last_modified: Option>, + pub last_modified: DateTime, pub name: String, pub override_keys: Vec, diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index c6ac1ad79..b3431ba99 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -56,7 +56,7 @@ pub mod cac_v1 { id -> Int8, created_at -> Timestamptz, created_by -> Text, - last_modified -> Nullable, + last_modified -> Timestamptz, name -> Text, override_keys -> Array, status -> ExperimentStatusType, diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml new file mode 100644 index 000000000..606a5bd7c --- /dev/null +++ b/crates/superposition_client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "superposition_client" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.26" +jsonlogic = "0.5.1" +reqwest = { version = "0.11.18", features = ["json"]} +serde = { version = "1.0.164", features = ["derive"] } +serde_json = "1.0.97" +tokio = {version = "1.29.1", features = ["full"]} +dotenv = "0.15.0" diff --git a/crates/superposition_client/README.md b/crates/superposition_client/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs new file mode 100644 index 000000000..447bb6e51 --- /dev/null +++ b/crates/superposition_client/src/lib.rs @@ -0,0 +1,140 @@ +mod types; +use std::{collections::HashMap, sync::Arc}; + +use chrono::{DateTime, TimeZone, Utc}; +use serde_json::Value; +use tokio::{ + sync::RwLock, + time::{self, Duration}, +}; +pub use types::{Config, Variants}; +use types::{ExperimentStore, Experiments, Variant}; + +#[derive(Clone, Debug)] +pub struct Client { + pub client_config: Arc, + pub(crate) experiments: Arc>, + pub(crate) http_client: reqwest::Client, + last_polled: Arc>>, +} + +//TODO: replace all unwraps with proper error handling +// DO NOT let panics show up in library + +impl Client { + pub fn new(config: Config) -> Self { + Client { + client_config: Arc::new(config), + experiments: Arc::new(RwLock::new(HashMap::new())), + http_client: reqwest::Client::new(), + last_polled: Arc::new(RwLock::new( + Utc.with_ymd_and_hms(2023, 01, 01, 0, 0, 0).unwrap(), + )), + } + } + + pub async fn run_polling_updates(self) { + let poll_interval = self.client_config.poll_frequency; + let hostname = &self.client_config.hostname; + let mut interval = time::interval(Duration::from_secs(poll_interval)); + let mut start_date = self.last_polled.write().await; + loop { + { + let experiments = get_experiments( + hostname.clone(), + self.http_client.clone(), + start_date.to_string(), + ) + .await + .unwrap(); + + let mut exp_store = self.experiments.write().await; + for (exp_id, experiment) in experiments.into_iter() { + match experiment.status { + types::ExperimentStatusType::CONCLUDED => { + exp_store.remove(&exp_id) + } + types::ExperimentStatusType::INPROGRESS => { + exp_store.insert(exp_id, experiment) + } + }; + } + } + *start_date = Utc::now(); + interval.tick().await; + } + } + + pub async fn get_applicable_variant( + &self, + contexts: &Value, + toss: u8, + ) -> Vec { + let running_experiments = self.experiments.read().await; + // try and if json logic works + let mut experiments: Experiments = Vec::new(); + for (_, exps) in running_experiments.iter() { + if let Ok(Value::Bool(true)) = jsonlogic::apply(&exps.context, contexts) { + experiments.push(exps.clone()); + } + } + + let mut variants: Vec = Vec::new(); + + for exp in experiments { + if let Some(v) = + self.decide_variant(exp.traffic_percentage, exp.variants, toss) + { + variants.push(v.id) + } + } + variants + } + + // decide which variant to return among all applicable experiments + fn decide_variant( + &self, + traffic: u8, + applicable_exps: Variants, + toss: u8, + ) -> Option { + let variant_count = applicable_exps.len() as u8; + let range = (traffic * variant_count) as u32; + if (toss as u32) > range { + return None + } + let buckets = (1..=variant_count) + .map(|i| traffic * i) + .collect::>(); + let index = buckets.into_iter().position(|x| toss < x); + applicable_exps.get(index.unwrap()).map(|x| x.clone()) + } +} + +async fn get_experiments( + hostname: String, + http_client: reqwest::Client, + start_date: String, +) -> Result { + let mut curr_exp_store: ExperimentStore = HashMap::new(); + let now = Utc::now(); + let endpoint = format!( + "{hostname}/experiments?from_date={start_date}&to_date={now}&page=1&count=100" + ); + let experiments = http_client + .get(format!("{endpoint}&status=INPROGRESS,CONCLUDED")) + .send() + .await + .unwrap() + .json::() + .await + .unwrap_or_default(); + + // println!("got these running experiments: {:?}", running_experiments); + + for experiments in experiments.into_iter() { + curr_exp_store.insert(experiments.id.to_string(), experiments); + } + + Ok(curr_exp_store) +} diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs new file mode 100644 index 000000000..65eaa23a5 --- /dev/null +++ b/crates/superposition_client/src/types.rs @@ -0,0 +1,38 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug)] +pub struct Config { + pub hostname: String, + pub poll_frequency: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub(crate) enum ExperimentStatusType { + INPROGRESS, + CONCLUDED, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub overrides: Value, +} + +pub type Variants = Vec; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct Experiment { + pub(crate) variants: Variants, + pub(crate) name: String, + pub(crate) id: i64, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, +} + +pub(crate) type Experiments = Vec; + +pub(crate) type ExperimentStore = HashMap; diff --git a/crates/superposition_client_integration_example/Cargo.toml b/crates/superposition_client_integration_example/Cargo.toml new file mode 100644 index 000000000..2eade68df --- /dev/null +++ b/crates/superposition_client_integration_example/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +superposition_client = { path = "../superposition_client" } +chrono = "0.4.26" + +# Https server framework +actix = "0.13.0" +actix-web = "4.0.0" +serde_json = "1.0.97" diff --git a/crates/superposition_client_integration_example/src/main.rs b/crates/superposition_client_integration_example/src/main.rs new file mode 100644 index 000000000..3a50d25f3 --- /dev/null +++ b/crates/superposition_client_integration_example/src/main.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, rt, + web::{get, Data, Path}, + App, HttpResponse, HttpServer, +}; + +use serde_json::json; +use superposition_client as exp; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let client_configuration = exp::Config { + hostname: "http://localhost:8080".to_string(), + poll_frequency: 10, + }; + let client = exp::Client::new(client_configuration); + rt::spawn(client.clone().run_polling_updates()); + HttpServer::new(move || { + App::new() + .app_data(Data::new(client.clone())) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + .service(get_variants) + }) + .bind(("127.0.0.1", 8082))? + .run() + .await +} + +#[get("/variants/{client_id}/{platform}/{toss}")] +async fn get_variants( + state: Data, + path: Path<(String, String, u8)>, +) -> HttpResponse { + let (client_id, platform, toss) = path.into_inner(); + println!("client state on the server = {:?}", state); + let contexts = json!({ + "clientId": client_id, + "os": platform + }); + let variant = state.get_applicable_variant(&contexts, toss).await; + println!("variant value: {:?}", variant); + HttpResponse::Ok().body("check your console") +} diff --git a/docker-compose.yaml b/docker-compose.yaml index acfa5691b..d0589f638 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,26 +1,6 @@ version: "3.4" services: - application: - container_name: "application" - command: ./context-aware-config - build: - dockerfile: Dockerfile - context: . - links: - - postgres - depends_on: - - postgres - environment: - # to access postgres outside of docker, use postgres://postgres:docker@localhost:5432 instead - - DATABASE_URL=postgres://postgres:docker@postgres:5432/config?sslmode=disable - - DB_POOL_SIZE=10 - - network_mode: bridge - tty: true - ports: - - "8080:8080" - postgres: build: ./docker-compose/postgres/ container_name: context-aware-config_postgres diff --git a/flake.nix b/flake.nix index 1d0b19e16..217066043 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,11 @@ # For `nix develop`: devShell = pkgs.mkShell { RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + # bring your local shell properties into nix env + shellHook = '' + echo "you are now in the nix shell" + eval $($SHELL) + ''; nativeBuildInputs = let univPkgs = with pkgs; [ diff --git a/libs/rust/Cargo.lock b/libs/rust/Cargo.lock deleted file mode 100644 index 291126140..000000000 --- a/libs/rust/Cargo.lock +++ /dev/null @@ -1,1811 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash 0.8.3", - "base64", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash 0.7.6", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time 0.3.22", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "bytestring" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" -dependencies = [ - "bytes", -] - -[[package]] -name = "cac" -version = "0.1.0" -dependencies = [ - "actix-web", - "chrono", - "jsonlogic", - "reqwest", - "serde", - "serde_json", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time 0.3.22", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "h2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "0.14.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipnet" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonlogic" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0699c6109316a5add3d42ae21ae954f207739f12b68914a0d447b9aa45e6f" -dependencies = [ - "serde_json", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.146" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "openssl" -version = "0.10.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "schannel" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" -dependencies = [ - "windows-sys 0.42.0", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "security-framework" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "serde_json" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" -dependencies = [ - "autocfg", - "cfg-if", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" -dependencies = [ - "autocfg", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "zstd" -version = "0.12.3+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/makefile b/makefile index 5d31a68cd..8790561f1 100644 --- a/makefile +++ b/makefile @@ -2,8 +2,18 @@ IMAGE_NAME ?= context-aware-config SHELL := /usr/bin/env bash +setup: + touch ./docker-compose/localstack/export_cyphers.sh + docker-compose up -d + cp .env.example .env + sleep 10 #TODO move this sleep to aws cli list-keys command instead + +db-init: + diesel migration run --config-file=crates/context-aware-config/diesel.toml + diesel migration run --config-file=crates/experimentation-platform/diesel.toml + build: - cargo build + cargo build --color always ci-test: npm ci --loglevel=error @@ -25,18 +35,19 @@ registry-login: --username AWS \ --password-stdin $(REGISTRY_HOST) -run: - pkill -f target/debug/context-aware-config & - touch ./docker-compose/localstack/export_cyphers.sh - docker-compose up -d postgres localstack - cp .env.example .env - #NOTE need to sleep here because locastack takes some time to internally - #populate the kms keyId - sleep 10 #TODO move this sleep to aws cli list-keys command instead - diesel migration run --config-file=crates/context-aware-config/diesel.toml - diesel migration run --config-file=crates/experimentation-platform/diesel.toml - cargo build --color always +cac: source ./docker-compose/localstack/export_cyphers.sh && \ - cargo run --color always + cargo run --package context-aware-config --color always + +example: + cargo run --package example + +ls-packages: + cargo run --package + +kill: + pkill -f target/debug/context-aware-config & + +run: kill setup db-init build cac default: build From df2dab6470b1f482c70660d6f571e808068a1670 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 3 Aug 2023 19:09:51 +0530 Subject: [PATCH 073/352] added experiment response type with string id --- .gitignore | 5 +- .../src/api/experiments/handlers.rs | 49 +++++++++------ .../src/api/experiments/types.rs | 61 ++++++++++++++++--- .../experimentation-platform/src/schema.patch | 2 +- flake.nix | 8 +-- 5 files changed, 92 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index d0f7e44f1..2c34c41fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Node artifact files +node_modules/ + # Log files *.log @@ -23,4 +26,4 @@ docker-compose/localstack/export_cyphers.sh test_logs .keep .vscode -*.session.sql +*.session.sql \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index cbebe017d..63bb851e5 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -17,12 +17,13 @@ use super::{ }, types::{ ConcludeExperimentRequest, ContextAction, ContextPutReq, ContextPutResp, - ExperimentCreateRequest, ExperimentCreateResponse, Variant, RampRequest, + ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, + ExperimentsResponse, RampRequest, Variant, }, }; use crate::{ api::{errors::AppError, experiments::types::ListFilters}, - db::models::{Experiment, ExperimentStatusType, Experiments}, + db::models::{Experiment, ExperimentStatusType}, }; pub fn endpoints() -> Scope { @@ -166,9 +167,7 @@ async fn create( match insert { Ok(mut inserted_experiments) => { let inserted_experiment: Experiment = inserted_experiments.remove(0); - let response = ExperimentCreateResponse { - experiment_id: inserted_experiment.id, - }; + let response = ExperimentCreateResponse::from(inserted_experiment); return Ok(Json(response)); } @@ -187,8 +186,8 @@ async fn conclude( path: web::Path, req: web::Json, db_conn: DbConnection, - _auth_info: AuthenticationInfo -) -> actix_web::Result> { + _auth_info: AuthenticationInfo, +) -> actix_web::Result> { use crate::db::schema::cac_v1::experiments::dsl; let experiment_id: i64 = path.into_inner(); @@ -282,7 +281,7 @@ async fn conclude( match experiment_update_result { Ok(updated_experiment) => { - return Ok(Json(updated_experiment)); + return Ok(Json(ExperimentResponse::from(updated_experiment))); } Err(e) => { log::info!("Failed to updated experiment status: {e}"); @@ -295,7 +294,7 @@ async fn conclude( async fn list_experiments( state: Data, filters: Query, -) -> actix_web::Result, AppError> { +) -> actix_web::Result, AppError> { let mut conn = match state.db_pool.get() { Ok(conn) => conn, Err(e) => { @@ -321,7 +320,14 @@ async fn list_experiments( let db_result = query.load::(&mut conn); match db_result { - Ok(response) => return Ok(Json(response)), + Ok(response) => { + return Ok(Json( + response + .into_iter() + .map(|entry| ExperimentResponse::from(entry)) + .collect(), + )) + } Err(e) => { return Err(match e { diesel::result::Error::NotFound => AppError { @@ -343,16 +349,18 @@ async fn list_experiments( async fn get_experiment( params: web::Path, db_conn: DbConnection, -) -> actix_web::Result> { +) -> actix_web::Result> { use crate::db::schema::cac_v1::experiments::dsl::*; let experiment_id = params.into_inner(); let DbConnection(mut conn) = db_conn; - let db_result = experiments.find(experiment_id).get_result(&mut conn); + let db_result = experiments + .find(experiment_id) + .get_result::(&mut conn); let response = match db_result { - Ok(result) => result, + Ok(result) => ExperimentResponse::from(result), Err(diesel::result::Error::NotFound) => { return Err(actix_web::error::ErrorNotFound( "Experiment not found".to_string(), @@ -372,14 +380,13 @@ async fn ramp( params: web::Path, req: web::Json, db_conn: DbConnection, -) -> actix_web::Result> { +) -> actix_web::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); use crate::db::schema::cac_v1::experiments::dsl::*; - let db_result: Result = experiments - .find(exp_id) - .get_result::(&mut conn); + let db_result: Result = + experiments.find(exp_id).get_result::(&mut conn); let experiment = match db_result { Ok(result) => result, @@ -402,14 +409,18 @@ async fn ramp( let max = 100 / variants_count; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { - return Err(actix_web::error::ErrorBadRequest("Experiment is already concluded")); + return Err(actix_web::error::ErrorBadRequest( + "Experiment is already concluded", + )); } else if new_traffic_percentage > max { log::info!("The Traffic percentage provided exceeds the range"); return Err(actix_web::error::ErrorBadRequest( format!("The traffic_percentage cannot exceed {}", max), )); } else if new_traffic_percentage == old_traffic_percentage { - return Err(actix_web::error::ErrorBadRequest("The traffic_percentage is same as provided")); + return Err(actix_web::error::ErrorBadRequest( + "The traffic_percentage is same as provided", + )); } let update = diesel::update(experiments) diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 47fccc310..8a69d7389 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -7,7 +7,7 @@ use serde::{ }; use serde_json::Value; -use crate::db::models::ExperimentStatusType; +use crate::db::models; #[derive(Deserialize, Serialize, Clone)] pub enum VariantType { @@ -38,10 +38,57 @@ pub struct ExperimentCreateRequest { #[derive(Serialize)] pub struct ExperimentCreateResponse { - pub experiment_id: i64, + pub experiment_id: String, } -/********** Experiment Create Req Types END ************/ +impl From for ExperimentCreateResponse { + fn from(experiment: models::Experiment) -> Self { + ExperimentCreateResponse { + experiment_id: experiment.id.to_string(), + } + } +} + +/********** Experiment Response Type **************/ +// Same as models::Experiments but `id` field is String +// JS have limitation of 53-bit integers, so on +// deserializing from JSON to JS Object will lead incorrect `id` values +#[derive(Serialize)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime, + pub created_by: String, + pub last_modified: DateTime, + + pub name: String, + pub override_keys: Vec, + pub status: models::ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, +} + +impl From for ExperimentResponse { + fn from(experiment: models::Experiment) -> Self { + ExperimentResponse { + id: experiment.id.to_string(), + created_at: experiment.created_at, + created_by: experiment.created_by, + last_modified: experiment.last_modified, + + name: experiment.name, + override_keys: experiment.override_keys, + status: experiment.status, + traffic_percentage: experiment.traffic_percentage, + + context: experiment.context, + variants: experiment.variants, + } + } +} + +pub type ExperimentsResponse = Vec; /********** Experiment Conclude Req Types **********/ @@ -50,8 +97,6 @@ pub struct ConcludeExperimentRequest { pub winner_variant: String, } -/********** Experiment Conclude Req Types END **********/ - /********** Context Bulk API Type *************/ #[derive(Deserialize, Serialize, Clone)] @@ -74,12 +119,12 @@ pub struct ContextPutResp { pub priority: i32, } -/********** Context Bulk API Type *************/ +/********** List API Filter Type *************/ #[derive(Deserialize, Debug)] pub struct ListFilters { #[serde(deserialize_with = "deserialize_stringified_list")] - pub status: Vec, + pub status: Vec, pub from_date: DateTime, pub to_date: DateTime, pub page: i64, @@ -121,7 +166,7 @@ where deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) } -#[derive(Deserialize,Debug)] +#[derive(Deserialize, Debug)] pub struct RampRequest { pub traffic_percentage: u64, } diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index b1c1c9218..406384b34 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -33,7 +33,7 @@ index ca23ca2..e6083a7 100644 id -> Int8, created_at -> Timestamptz, created_by -> Text, - last_modified -> Nullable, + last_modified -> Timestamptz, name -> Text, - override_keys -> Array>, + override_keys -> Array, diff --git a/flake.nix b/flake.nix index 217066043..57ed17d47 100644 --- a/flake.nix +++ b/flake.nix @@ -25,10 +25,10 @@ devShell = pkgs.mkShell { RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; # bring your local shell properties into nix env - shellHook = '' - echo "you are now in the nix shell" - eval $($SHELL) - ''; + # shellHook = '' + # echo "you are now in the nix shell" + # eval $($SHELL) + # ''; nativeBuildInputs = let univPkgs = with pkgs; [ From 34ba4d44845fb79fa7e9f3285bd0d9d3eaf3b0df Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Fri, 4 Aug 2023 13:53:25 +0530 Subject: [PATCH 074/352] feat: add support for last modified by in experiments --- .../down.sql | 4 + .../2023-08-04-080245_add-modified-by/up.sql | 5 ++ .../src/api/experiments/handlers.rs | 88 ++++++++++++------- .../experimentation-platform/src/db/models.rs | 1 + .../experimentation-platform/src/db/schema.rs | 1 + 5 files changed, 65 insertions(+), 34 deletions(-) create mode 100644 crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql diff --git a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql new file mode 100644 index 000000000..30947823d --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE cac_v1.experiments +DROP COLUMN last_modified_by; diff --git a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql new file mode 100644 index 000000000..430339eb0 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here + +-- Your SQL goes here +ALTER TABLE cac_v1.experiments +ADD COLUMN last_modified_by TEXT NOT NULL DEFAULT 'Null'; diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 63bb851e5..4de0c9417 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -149,7 +149,7 @@ async fn create( let AuthenticationInfo(email) = auth_info; let new_experiment = Experiment { id: experiment_id, - created_by: email, + created_by: email.to_string(), created_at: Utc::now(), last_modified: Utc::now(), name: req.name.to_string(), @@ -158,6 +158,7 @@ async fn create( status: ExperimentStatusType::CREATED, context: req.context.clone(), variants: serde_json::to_value(variants).unwrap(), + last_modified_by: email, }; let insert = diesel::insert_into(experiments) @@ -186,7 +187,7 @@ async fn conclude( path: web::Path, req: web::Json, db_conn: DbConnection, - _auth_info: AuthenticationInfo, + auth_info: AuthenticationInfo, ) -> actix_web::Result> { use crate::db::schema::cac_v1::experiments::dsl; @@ -225,9 +226,9 @@ async fn conclude( let mut operations: Vec = vec![]; let experiment_variants: Vec = serde_json::from_value(experiment.variants) .map_err(|e| { - log::error!("parsing to variant type failed with err: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + log::error!("parsing to variant type failed with err: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; let mut is_valid_winner_variant = false; for variant in experiment_variants { @@ -270,12 +271,15 @@ async fn conclude( return Err(actix_web::error::ErrorInternalServerError("")); } + let AuthenticationInfo(email) = auth_info; + // updating experiment status in db let experiment_update_result = diesel::update(dsl::experiments) .filter(dsl::id.eq(experiment_id)) .set(( dsl::status.eq(ExperimentStatusType::CONCLUDED), dsl::last_modified.eq(Utc::now()), + dsl::last_modified_by.eq(email), )) .get_result::(&mut conn); @@ -328,21 +332,20 @@ async fn list_experiments( .collect(), )) } + Err(diesel::result::Error::NotFound) => Err(AppError { + message: String::from("No results found for your query"), + possible_fix: String::from("Update your filter parameters"), + status_code: StatusCode::NOT_FOUND, + }), Err(e) => { - return Err(match e { - diesel::result::Error::NotFound => AppError { - message: String::from("No results found"), - possible_fix: String::from("Update your filter parameters"), - status_code: StatusCode::NOT_FOUND, - }, - _ => AppError { - message: String::from("Something went wrong"), - possible_fix: String::from("Please try again later"), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }, + println!("Error occurred in list experiments API {:?}", e); + Err(AppError { + message: String::from("Something went wrong"), + possible_fix: String::from("Please try again later"), + status_code: StatusCode::INTERNAL_SERVER_ERROR, }) } - }; + } } #[get("/{id}")] @@ -380,6 +383,7 @@ async fn ramp( params: web::Path, req: web::Json, db_conn: DbConnection, + auth_info: AuthenticationInfo, ) -> actix_web::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); @@ -390,11 +394,14 @@ async fn ramp( let experiment = match db_result { Ok(result) => result, - Err(diesel::result::Error::NotFound) => - return Err(actix_web::error::ErrorNotFound("No results found")), + Err(diesel::result::Error::NotFound) => { + return Err(actix_web::error::ErrorNotFound("No results found")) + } Err(e) => { log::info!("{e}"); - return Err(actix_web::error::ErrorInternalServerError("Something went wrong")); + return Err(actix_web::error::ErrorInternalServerError( + "Something went wrong", + )); } }; @@ -402,9 +409,9 @@ async fn ramp( let new_traffic_percentage = req.traffic_percentage as u8; let experiment_variants: Vec = serde_json::from_value(experiment.variants) .map_err(|e| { - log::error!("parsing to variant type failed with err: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + log::error!("parsing to variant type failed with err: {e}"); + actix_web::error::ErrorInternalServerError("") + })?; let variants_count = experiment_variants.len() as u8; let max = 100 / variants_count; @@ -414,30 +421,43 @@ async fn ramp( )); } else if new_traffic_percentage > max { log::info!("The Traffic percentage provided exceeds the range"); - return Err(actix_web::error::ErrorBadRequest( - format!("The traffic_percentage cannot exceed {}", max), - )); + return Err(actix_web::error::ErrorBadRequest(format!( + "The traffic_percentage cannot exceed {}", + max + ))); } else if new_traffic_percentage == old_traffic_percentage { return Err(actix_web::error::ErrorBadRequest( "The traffic_percentage is same as provided", )); } + let AuthenticationInfo(email) = auth_info; let update = diesel::update(experiments) .filter(id.eq(exp_id)) .set(( - traffic_percentage.eq(req.traffic_percentage as i32), - last_modified.eq(Utc::now()), - )) + traffic_percentage.eq(req.traffic_percentage as i32), + last_modified.eq(Utc::now()), + last_modified_by.eq(email), + )) .execute(&mut conn); match update { - Ok(0) => return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")), - Ok(_) => return Ok(Json(format!("Traffic percentage has been updated for the experiment id : {}", exp_id))), - Err(e) => { + Ok(0) => { + return Err(actix_web::error::ErrorInternalServerError( + "Failed to update the traffic_percentage", + )) + } + Ok(_) => { + return Ok(Json(format!( + "Traffic percentage has been updated for the experiment id : {}", + exp_id + ))) + } + Err(e) => { log::info!("Failed to update the traffic_percentage: {e}"); - return Err(actix_web::error::ErrorInternalServerError("Failed to update the traffic_percentage")); + return Err(actix_web::error::ErrorInternalServerError( + "Failed to update the traffic_percentage", + )); } } } - diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index c4ec8dd6c..1db0dc4af 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -32,6 +32,7 @@ pub struct Experiment { pub context: Value, pub variants: Value, + pub last_modified_by: String, } pub type Experiments = Vec; diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index b3431ba99..819943942 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -63,6 +63,7 @@ pub mod cac_v1 { traffic_percentage -> Int4, context -> Json, variants -> Json, + last_modified_by -> Text, } } From 1281ec7b8d5a04345951f78fff228f94d6eb5d45 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Mon, 7 Aug 2023 11:29:06 +0530 Subject: [PATCH 075/352] fix: minor fixes for exp client --- crates/superposition_client/src/lib.rs | 23 +++++++++++++---------- crates/superposition_client/src/types.rs | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 447bb6e51..4fb47a934 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -39,6 +39,9 @@ impl Client { let mut interval = time::interval(Duration::from_secs(poll_interval)); let mut start_date = self.last_polled.write().await; loop { + // NOTE: this additional block scopes the write lock + // at the end of this block, the write lock on exp store is released + // allowing other threads to read updated data { let experiments = get_experiments( hostname.clone(), @@ -59,7 +62,7 @@ impl Client { } }; } - } + } // write lock on exp store releases here *start_date = Utc::now(); interval.tick().await; } @@ -67,15 +70,15 @@ impl Client { pub async fn get_applicable_variant( &self, - contexts: &Value, + context: &Value, toss: u8, ) -> Vec { let running_experiments = self.experiments.read().await; // try and if json logic works let mut experiments: Experiments = Vec::new(); - for (_, exps) in running_experiments.iter() { - if let Ok(Value::Bool(true)) = jsonlogic::apply(&exps.context, contexts) { - experiments.push(exps.clone()); + for (_, exp) in running_experiments.iter() { + if let Ok(Value::Bool(true)) = jsonlogic::apply(&exp.context, context) { + experiments.push(exp.clone()); } } @@ -95,10 +98,10 @@ impl Client { fn decide_variant( &self, traffic: u8, - applicable_exps: Variants, + applicable_vars: Variants, toss: u8, ) -> Option { - let variant_count = applicable_exps.len() as u8; + let variant_count = applicable_vars.len() as u8; let range = (traffic * variant_count) as u32; if (toss as u32) > range { return None @@ -107,7 +110,7 @@ impl Client { .map(|i| traffic * i) .collect::>(); let index = buckets.into_iter().position(|x| toss < x); - applicable_exps.get(index.unwrap()).map(|x| x.clone()) + applicable_vars.get(index.unwrap()).map(|x| x.clone()) } } @@ -132,8 +135,8 @@ async fn get_experiments( // println!("got these running experiments: {:?}", running_experiments); - for experiments in experiments.into_iter() { - curr_exp_store.insert(experiments.id.to_string(), experiments); + for experiment in experiments.into_iter() { + curr_exp_store.insert(experiment.id.to_string(), experiment); } Ok(curr_exp_store) diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index 65eaa23a5..d251cccbe 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -27,7 +27,7 @@ pub type Variants = Vec; pub(crate) struct Experiment { pub(crate) variants: Variants, pub(crate) name: String, - pub(crate) id: i64, + pub(crate) id: String, pub(crate) traffic_percentage: u8, pub(crate) context: Value, pub(crate) status: ExperimentStatusType, From 73a4eff0ef8303af6c20b3f1fc47e84ce31a2dfc Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 7 Aug 2023 14:42:32 +0530 Subject: [PATCH 076/352] ci: regenerated schema.patch with latest schema.rs --- .../experimentation-platform/src/schema.patch | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index 406384b34..155a6b6af 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -1,11 +1,8 @@ diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs -index ca23ca2..e6083a7 100644 +index ef6ee2d..8199439 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs -@@ -6,16 +6,12 @@ pub mod cac_v1 { - #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] - pub struct DimensionType; - +@@ -9,10 +9,6 @@ pub mod cac_v1 { #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] pub struct ExperimentStatusType; @@ -14,24 +11,17 @@ index ca23ca2..e6083a7 100644 - #[diesel(postgres_type(name = "not_null_text", schema = "cac_v1"))] - pub struct NotNullText; } - + diesel::table! { - cac_v1.contexts (id) { - id -> Varchar, - value -> Json, -@@ -51,22 +47,21 @@ pub mod cac_v1 { - created_by -> Varchar, - } - } - +@@ -54,7 +50,6 @@ pub mod cac_v1 { + diesel::table! { use diesel::sql_types::*; - use super::sql_types::NotNullText; use super::sql_types::ExperimentStatusType; - + cac_v1.experiments (id) { - id -> Int8, - created_at -> Timestamptz, +@@ -63,7 +58,7 @@ pub mod cac_v1 { created_by -> Text, last_modified -> Timestamptz, name -> Text, @@ -40,6 +30,3 @@ index ca23ca2..e6083a7 100644 status -> ExperimentStatusType, traffic_percentage -> Int4, context -> Json, - variants -> Json, - } - } From a0ded0601a68fac43a05c970487350f86f7db001 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 8 Aug 2023 15:57:44 +0530 Subject: [PATCH 077/352] fix: - changed status to inprogress in case of a ramp call - updated range check in experimentation client --- .../src/api/experiments/handlers.rs | 2 ++ .../experimentation-platform/src/api/experiments/helpers.rs | 6 +++--- crates/superposition_client/src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 4de0c9417..019cdae9d 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -415,6 +415,7 @@ async fn ramp( let variants_count = experiment_variants.len() as u8; let max = 100 / variants_count; + if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { return Err(actix_web::error::ErrorBadRequest( "Experiment is already concluded", @@ -438,6 +439,7 @@ async fn ramp( traffic_percentage.eq(req.traffic_percentage as i32), last_modified.eq(Utc::now()), last_modified_by.eq(email), + status.eq(ExperimentStatusType::INPROGRESS) )) .execute(&mut conn); diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index f58e63365..c01d49a5a 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -216,9 +216,9 @@ pub fn add_variant_dimension_to_ctx( }; let variant_condition = serde_json::json!({ - "==" : [ - { "var": "variant" }, - variant + "in" : [ + variant, + { "var": "variantIds" } ] }); conditions.push(variant_condition); diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 4fb47a934..7648e9962 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -103,7 +103,7 @@ impl Client { ) -> Option { let variant_count = applicable_vars.len() as u8; let range = (traffic * variant_count) as u32; - if (toss as u32) > range { + if (toss as u32) >= range { return None } let buckets = (1..=variant_count) From 40a9a0780f2e1f7eb724726c9472d402e4c488a1 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 9 Aug 2023 16:18:11 +0530 Subject: [PATCH 078/352] ci: enabling tests in pr builds --- .env.example | 7 +- Jenkinsfile | 9 +-- crates/context-aware-config/src/db/schema.rs | 1 - crates/service-utils/src/aws/kms.rs | 2 +- docker-compose.yaml | 2 - docker-compose/localstack/Dockerfile | 4 -- docker-compose/localstack/get_db_password.sh | 15 ++++ docker-compose/localstack/localstack_setup.sh | 19 ----- flake.nix | 1 + makefile | 71 ++++++++++++------- 10 files changed, 72 insertions(+), 59 deletions(-) create mode 100755 docker-compose/localstack/get_db_password.sh delete mode 100644 docker-compose/localstack/localstack_setup.sh diff --git a/.env.example b/.env.example index a13da479d..f1fbd5e6a 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,16 @@ -DATABASE_URL=postgres://postgres:docker@127.0.0.1:5432/config?sslmode=disable +DATABASE_URL=postgres://postgres:docker@dockerdns:5432/config?sslmode=disable RUST_LOG=info AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_SESSION_TOKEN=test AWS_REGION=ap-south-1 DB_USER=postgres -DB_HOST=127.0.0.1:5432 +DB_HOST=dockerdns:5432 DB_NAME=config APP_ENV=DEV ADMIN_TOKEN="12345678" -CAC_HOST="http://localhost:8080" +AWS_REGION_ENDPOINT=http://dockerdns:4566 ALLOW_SAME_KEYS_OVERLAPPING_CTX=true ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true +CAC_HOST="http://localhost:8080" diff --git a/Jenkinsfile b/Jenkinsfile index 4751f97c6..e85f310fb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,6 +10,7 @@ pipeline { REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; + DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" } stages { stage('Checkout') { @@ -25,10 +26,10 @@ pipeline { } } -// stage('Test') { -// when { expression { SKIP_CI == 'false' } } -// steps { sh 'make ci-test' } -// } + stage('Test') { + when { expression { SKIP_CI == 'false' } } + steps { sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' } + } stage('Build Image') { when { diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 12081af33..992f759d9 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -44,7 +44,6 @@ pub mod cac_v1 { } } - diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, diff --git a/crates/service-utils/src/aws/kms.rs b/crates/service-utils/src/aws/kms.rs index 917ceb15c..d42c9e132 100644 --- a/crates/service-utils/src/aws/kms.rs +++ b/crates/service-utils/src/aws/kms.rs @@ -36,7 +36,7 @@ pub fn new_client() -> KmsClient { let kms_region = match app_env.as_str() { "DEV" => Region::Custom { name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), - endpoint: "http://localhost:4566".to_owned(), + endpoint: get_from_env_unsafe("AWS_REGION_ENDPOINT").unwrap_or(String::from("http://localhost:4566")), }, _ => get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1), }; diff --git a/docker-compose.yaml b/docker-compose.yaml index d0589f638..e221cbec1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -24,5 +24,3 @@ services: LOCALSTACK_SERVICES: s3, sns, sqs, logs, cloudwatch, kms AWS_DEFAULT_REGION: ap-south-1 EDGE_PORT: 4566 - volumes: - - ./docker-compose/localstack/export_cyphers.sh:/etc/localstack/export_cyphers.sh diff --git a/docker-compose/localstack/Dockerfile b/docker-compose/localstack/Dockerfile index ed33381cf..41570683e 100644 --- a/docker-compose/localstack/Dockerfile +++ b/docker-compose/localstack/Dockerfile @@ -3,7 +3,3 @@ FROM localstack/localstack:1.3.0 RUN aws configure set aws_access_key_id test RUN aws configure set aws_secret_access_key test RUN aws configure set default.region ap-south-1 -RUN apt-get install -y jq - -COPY ./localstack_setup.sh /etc/localstack/init/ready.d/localstack_setup.sh -RUN chmod +x /etc/localstack/init/ready.d/localstack_setup.sh diff --git a/docker-compose/localstack/get_db_password.sh b/docker-compose/localstack/get_db_password.sh new file mode 100755 index 000000000..e67449104 --- /dev/null +++ b/docker-compose/localstack/get_db_password.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# CONSTANTS +region="ap-south-1" +alias aws="aws --endpoint-url=http://$DOCKER_DNS:4566 --region=${region}" + +# ****** KMS ******* + +secret_key_id=`aws kms create-key | jq -r .KeyMetadata.KeyId` + +kms_encrypt(){ + echo $(aws kms encrypt --key-id $secret_key_id --plaintext $2 | jq -r .CiphertextBlob) +} + +kms_encrypt DB_PASSWORD docker diff --git a/docker-compose/localstack/localstack_setup.sh b/docker-compose/localstack/localstack_setup.sh deleted file mode 100644 index 617aba0e8..000000000 --- a/docker-compose/localstack/localstack_setup.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -mkdir /alloo -# CONSTANTS - -region="ap-south-1" -alias aws="aws --endpoint-url=http://localhost:4566 --region=${region}" - -# ****** KMS ******* - -echo "#!/bin/sh" > /etc/localstack/export_cyphers.sh -secret_key_id=`aws kms create-key | jq -r .KeyMetadata.KeyId` - -kms_encrypt(){ - cypher=`aws kms encrypt --key-id $secret_key_id --plaintext $2 | jq -r .CiphertextBlob` - echo "export $1=$cypher" >> /etc/localstack/export_cyphers.sh -} - -kms_encrypt DB_PASSWORD docker diff --git a/flake.nix b/flake.nix index 57ed17d47..a3f3e2bf8 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,7 @@ docker-compose stdenv.cc pkgconfig + awscli ]; darwinPkgs = with pkgs; [ darwin.apple_sdk.frameworks.Security diff --git a/makefile b/makefile index 8790561f1..23506e5a2 100644 --- a/makefile +++ b/makefile @@ -1,25 +1,60 @@ IMAGE_NAME ?= context-aware-config - +DOCKER_DNS ?= localhost SHELL := /usr/bin/env bash -setup: - touch ./docker-compose/localstack/export_cyphers.sh - docker-compose up -d - cp .env.example .env - sleep 10 #TODO move this sleep to aws cli list-keys command instead +.PHONY: + db-init + setup + kill + run + ci-test + ci-build + ci-push + registry-login + validate-aws-connection + validate-psql-connection + cac db-init: diesel migration run --config-file=crates/context-aware-config/diesel.toml diesel migration run --config-file=crates/experimentation-platform/diesel.toml -build: +validate-aws-connection: + aws --endpoint-url=http://$(DOCKER_DNS):4566 --region=ap-south-1 sts get-caller-identity + +validate-psql-connection: + pg_isready -h $(DOCKER_DNS) -p 5432 + +setup: + docker-compose up -d postgres localstack + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + make db-init + +kill: + -pkill -f target/debug/context-aware-config & + +cac: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo run --color always --bin context-aware-config + +run: + -make kill cargo build --color always + make setup + make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: - npm ci --loglevel=error -docker rm -f $$(docker container ls --filter name=^context-aware-config -q) - make run 2>&1 | tee test_logs & - while ! grep -q "starting in Actix" test_logs; do echo "waiting for bootup..." && sleep 2; done + npm ci --loglevel=error + make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & + while ! grep -q "starting in Actix" test_logs; \ + do echo "ci-test: waiting for bootup..." && sleep 4; \ + done npm run test ci-build: @@ -35,19 +70,5 @@ registry-login: --username AWS \ --password-stdin $(REGISTRY_HOST) -cac: - source ./docker-compose/localstack/export_cyphers.sh && \ - cargo run --package context-aware-config --color always - -example: - cargo run --package example - -ls-packages: - cargo run --package - -kill: - pkill -f target/debug/context-aware-config & - -run: kill setup db-init build cac -default: build +default: dev-build From e929a716e6640753a46d64d58c7370bf34f8b14d Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Wed, 9 Aug 2023 18:41:36 +0530 Subject: [PATCH 079/352] added get_experiment_state in superposition_client --- crates/superposition_client/src/lib.rs | 14 ++++++++------ crates/superposition_client/src/types.rs | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 7648e9962..06f7d17af 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -68,11 +68,7 @@ impl Client { } } - pub async fn get_applicable_variant( - &self, - context: &Value, - toss: u8, - ) -> Vec { + pub async fn get_applicable_variant(&self, context: &Value, toss: u8) -> Vec { let running_experiments = self.experiments.read().await; // try and if json logic works let mut experiments: Experiments = Vec::new(); @@ -94,6 +90,12 @@ impl Client { variants } + pub async fn get_running_experiments(&self) -> Experiments { + let running_experiments = self.experiments.read().await; + let experiments: Experiments = running_experiments.values().cloned().collect(); + experiments + } + // decide which variant to return among all applicable experiments fn decide_variant( &self, @@ -104,7 +106,7 @@ impl Client { let variant_count = applicable_vars.len() as u8; let range = (traffic * variant_count) as u32; if (toss as u32) >= range { - return None + return None; } let buckets = (1..=variant_count) .map(|i| traffic * i) diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index d251cccbe..189f1fed8 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -24,7 +24,7 @@ pub struct Variant { pub type Variants = Vec; #[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct Experiment { +pub struct Experiment { pub(crate) variants: Variants, pub(crate) name: String, pub(crate) id: String, @@ -33,6 +33,6 @@ pub(crate) struct Experiment { pub(crate) status: ExperimentStatusType, } -pub(crate) type Experiments = Vec; +pub type Experiments = Vec; pub(crate) type ExperimentStore = HashMap; From ce297defee2b8b18b7682816bbd4a87140694833 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 10 Aug 2023 16:01:45 +0530 Subject: [PATCH 080/352] fix: removed traffic-percentage from experiment create request --- .../src/api/experiments/handlers.rs | 11 +---------- .../src/api/experiments/types.rs | 1 - 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 019cdae9d..7b458d633 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -68,15 +68,6 @@ async fn create( )); } - //traffic_percentage should be max 100/length of variants - let allowed_traffic = (100 / variants.len()) as i32; - if req.traffic_percentage > allowed_traffic { - return Err(actix_web::error::ErrorBadRequest(format!( - "the traffic percentage specified ({}) is greater than what can be evenly distributed among variants. Reduce the traffic percentage to a value <= {}", - req.traffic_percentage, allowed_traffic - ))); - } - // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; match validate_experiment(&req, &flags, &mut conn) { @@ -154,7 +145,7 @@ async fn create( last_modified: Utc::now(), name: req.name.to_string(), override_keys: req.override_keys.to_vec(), - traffic_percentage: req.traffic_percentage, + traffic_percentage: 0, status: ExperimentStatusType::CREATED, context: req.context.clone(), variants: serde_json::to_value(variants).unwrap(), diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 8a69d7389..958ca0602 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -30,7 +30,6 @@ pub struct Variant { pub struct ExperimentCreateRequest { pub name: String, pub override_keys: Vec, - pub traffic_percentage: i32, pub context: Value, pub variants: Vec, From f051a0292e07ad1aebea7fb4f81faa19dd6f4f56 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 9 Aug 2023 16:28:26 +0530 Subject: [PATCH 081/352] fix: added total items to list API response - Added total items to experiments list API response - Added total page count to experiments list API response - Added optionality for list API --- .../src/api/experiments/handlers.rs | 64 ++++++++++++------- .../src/api/experiments/types.rs | 30 ++++++--- crates/superposition_client/src/lib.rs | 40 +++++++----- crates/superposition_client/src/types.rs | 7 ++ 4 files changed, 95 insertions(+), 46 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 7b458d633..9037b77be 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -5,7 +5,7 @@ use actix_web::{ web::{self, Data, Json, Query}, Scope, }; -use chrono::Utc; +use chrono::{Duration, Utc}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use service_utils::service::types::{AppState, AuthenticationInfo, DbConnection}; @@ -287,41 +287,62 @@ async fn conclude( #[get("")] async fn list_experiments( - state: Data, filters: Query, + db_conn: DbConnection, ) -> actix_web::Result, AppError> { - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, + let DbConnection(mut conn) = db_conn; + use crate::db::schema::cac_v1::experiments::dsl::*; + let query_builder = |filters: &ListFilters| { + let mut builder = experiments.into_boxed(); + if let Some(states) = filters.status.clone() { + builder = builder.filter(status.eq_any(states.0.clone())); + } + let now = Utc::now(); + builder + .filter(last_modified.ge(filters.from_date.unwrap_or(now - Duration::hours(24)))) + .filter(last_modified.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(last_modified.desc()) + .limit(limit) + .offset(offset); + + let number_of_experiments = match count_query.count().get_result(&mut conn) { + Ok(count) => count, + Err(diesel::result::Error::NotFound) => 0, Err(e) => { - log::info!("Unable to get db connection from pool, error: {e}"); + println!( + "Error occurred while counting items in list experiments API {:?}", + e + ); return Err(AppError { - message: "Something went wrong".to_string(), - possible_fix: "Try again after some time".to_string(), + message: String::from("Something went wrong"), + possible_fix: String::from("Please try again later"), status_code: StatusCode::INTERNAL_SERVER_ERROR, }); - // return an error } }; - use crate::db::schema::cac_v1::experiments::dsl::*; - let query = experiments - .filter(status.eq_any(filters.status.clone())) - .filter(last_modified.ge(filters.from_date)) - .filter(last_modified.le(filters.to_date)) - .order(last_modified.desc()) - .limit(filters.count) - .offset((filters.page - 1) * filters.count); - let db_result = query.load::(&mut conn); + let total_pages = (number_of_experiments as f64 / limit as f64).ceil() as i64; + match db_result { Ok(response) => { - return Ok(Json( - response + return Ok(Json(ExperimentsResponse { + total_items: number_of_experiments, + total_pages: total_pages, + data: response .into_iter() .map(|entry| ExperimentResponse::from(entry)) .collect(), - )) + })) } Err(diesel::result::Error::NotFound) => Err(AppError { message: String::from("No results found for your query"), @@ -406,7 +427,6 @@ async fn ramp( let variants_count = experiment_variants.len() as u8; let max = 100 / variants_count; - if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { return Err(actix_web::error::ErrorBadRequest( "Experiment is already concluded", @@ -430,7 +450,7 @@ async fn ramp( traffic_percentage.eq(req.traffic_percentage as i32), last_modified.eq(Utc::now()), last_modified_by.eq(email), - status.eq(ExperimentStatusType::INPROGRESS) + status.eq(ExperimentStatusType::INPROGRESS), )) .execute(&mut conn); diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 958ca0602..ee978cfc3 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -7,7 +7,7 @@ use serde::{ }; use serde_json::Value; -use crate::db::models; +use crate::db::models::{self, ExperimentStatusType}; #[derive(Deserialize, Serialize, Clone)] pub enum VariantType { @@ -87,7 +87,12 @@ impl From for ExperimentResponse { } } -pub type ExperimentsResponse = Vec; +#[derive(Serialize)] +pub struct ExperimentsResponse { + pub total_items: i64, + pub total_pages: i64, + pub data: Vec, +} /********** Experiment Conclude Req Types **********/ @@ -120,16 +125,23 @@ pub struct ContextPutResp { /********** List API Filter Type *************/ +#[derive(Deserialize, Debug, Clone)] +pub struct StatusTypes( + #[serde(deserialize_with = "deserialize_stringified_list")] + pub Vec +); + + #[derive(Deserialize, Debug)] pub struct ListFilters { - #[serde(deserialize_with = "deserialize_stringified_list")] - pub status: Vec, - pub from_date: DateTime, - pub to_date: DateTime, - pub page: i64, - pub count: i64, + pub status: Option, + pub from_date: Option>, + pub to_date: Option>, + pub page: Option, + pub count: Option, } + pub fn deserialize_stringified_list<'de, D, I>( deserializer: D, ) -> std::result::Result, D::Error> @@ -146,7 +158,7 @@ where type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string containing a list") + formatter.write_str("a string containing comma separated values eg: CREATED,INPROGRESS") } fn visit_str(self, v: &str) -> std::result::Result diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 06f7d17af..cf4d8b113 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -8,7 +8,7 @@ use tokio::{ time::{self, Duration}, }; pub use types::{Config, Variants}; -use types::{ExperimentStore, Experiments, Variant}; +use types::{ExperimentStore, Experiments, ListExperimentsResponse, Variant}; #[derive(Clone, Debug)] pub struct Client { @@ -122,23 +122,33 @@ async fn get_experiments( start_date: String, ) -> Result { let mut curr_exp_store: ExperimentStore = HashMap::new(); + let requesting_count = 10; + let mut page = 1; let now = Utc::now(); - let endpoint = format!( - "{hostname}/experiments?from_date={start_date}&to_date={now}&page=1&count=100" - ); - let experiments = http_client - .get(format!("{endpoint}&status=INPROGRESS,CONCLUDED")) - .send() - .await - .unwrap() - .json::() - .await - .unwrap_or_default(); + loop { + let endpoint = format!( + "{hostname}/experiments?from_date={start_date}&to_date={now}&page={page}&count={requesting_count}" + ); + let list_experiments_response = http_client + .get(format!("{endpoint}&status=INPROGRESS,CONCLUDED")) + .send() + .await + .unwrap() + .json::() + .await + .unwrap_or_default(); - // println!("got these running experiments: {:?}", running_experiments); + let experiments = list_experiments_response.data; + // println!("got these running experiments: {:?}", running_experiments); - for experiment in experiments.into_iter() { - curr_exp_store.insert(experiment.id.to_string(), experiment); + for experiment in experiments.into_iter() { + curr_exp_store.insert(experiment.id.to_string(), experiment); + } + if page < list_experiments_response.total_pages { + page += 1; + } else { + break; + } } Ok(curr_exp_store) diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index 189f1fed8..f11e9fa7d 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -36,3 +36,10 @@ pub struct Experiment { pub type Experiments = Vec; pub(crate) type ExperimentStore = HashMap; + +#[derive(Serialize, Deserialize, Default)] +pub(crate) struct ListExperimentsResponse { + pub(crate) total_items: i64, + pub(crate) total_pages: i64, + pub(crate) data: Experiments, +} From 611575947bdf99662be0731b95a573785cb421d9 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Wed, 9 Aug 2023 17:30:41 +0530 Subject: [PATCH 082/352] feat: added log table for all cac_v1 tables --- .../2023-08-09-111620_add_audit_log/down.sql | 6 + .../v1/2023-08-09-111620_add_audit_log/up.sql | 130 +++++++++++++ crates/context-aware-config/src/db/schema.rs | 182 ++++++++++++++++++ .../down.sql | 4 +- .../2023-08-09-081529_add_audit_log/down.sql | 3 + .../2023-08-09-081529_add_audit_log/up.sql | 4 + .../experimentation-platform/src/db/schema.rs | 182 ++++++++++++++++++ .../experimentation-platform/src/schema.patch | 2 +- 8 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql create mode 100644 crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql create mode 100644 crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql diff --git a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql new file mode 100644 index 000000000..7d1b18662 --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +DROP TRIGGER contexts_audit ON cac_v1.contexts; +DROP TRIGGER dimensions_audit ON cac_v1.dimensions; +DROP TRIGGER default_configs_audit ON cac_v1.default_configs; + +DROP TABLE cac_v1.event_log; diff --git a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql new file mode 100644 index 000000000..943461a04 --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql @@ -0,0 +1,130 @@ +-- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE TABLE cac_v1.event_log ( + id UUID DEFAULT uuid_generate_v4(), + table_name TEXT NOT NULL, + user_name TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + action TEXT NOT NULL, + original_data json, + new_data json, + query TEXT NOT NULL, + PRIMARY KEY(id, timestamp) +) PARTITION BY RANGE (timestamp); + +CREATE INDEX event_log_table_name_index ON cac_v1.event_log (table_name) INCLUDE (action, timestamp); +CREATE INDEX event_log_timestamp_index ON cac_v1.event_log (timestamp) INCLUDE (action, table_name); +CREATE INDEX event_log_action_index on cac_v1.event_log (action) INCLUDE (timestamp, table_name); + + +CREATE OR REPLACE FUNCTION cac_v1.event_logger() RETURNS TRIGGER AS $$ +DECLARE + old_data json; + new_data json; +BEGIN + IF (TG_OP = 'UPDATE') THEN + old_data := row_to_json(OLD); + new_data := row_to_json(NEW); + + INSERT INTO cac_v1.event_log + (table_name, user_name, action, original_data, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + new_data, + current_query() + ); + + ELSIF (TG_OP = 'DELETE') THEN + old_data := row_to_json(OLD); + + INSERT INTO cac_v1.event_log + (table_name, user_name, action, original_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + current_query() + ); + + ELSIF (TG_OP = 'INSERT') THEN + new_data = row_to_json(NEW); + + INSERT INTO cac_v1.event_log + (table_name, user_name, action, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + new_data, + current_query() + ); + END IF; + + RETURN NULL; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE TRIGGER contexts_audit +AFTER INSERT OR UPDATE OR DELETE ON cac_v1.contexts +FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); + +CREATE TRIGGER dimensions_audit +AFTER INSERT OR UPDATE OR DELETE ON cac_v1.dimensions +FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); + +CREATE TRIGGER default_configs_audit +AFTER INSERT OR UPDATE OR DELETE ON cac_v1.default_configs +FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); + +-- Partitions for event_log table +CREATE TABLE cac_v1.event_log_y2023m08 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2023-08-01') TO ('2023-09-01'); + +CREATE TABLE cac_v1.event_log_y2023m09 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2023-09-01') TO ('2023-10-01'); + +CREATE TABLE cac_v1.event_log_y2023m10 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2023-10-01') TO ('2023-11-01'); + +CREATE TABLE cac_v1.event_log_y2023m11 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2023-11-01') TO ('2023-12-01'); + +CREATE TABLE cac_v1.event_log_y2023m12 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2023-12-01') TO ('2024-01-01'); + +CREATE TABLE cac_v1.event_log_y2024m01 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-01-01') TO ('2024-02-01'); + +CREATE TABLE cac_v1.event_log_y2024m02 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-02-01') TO ('2024-03-01'); + +CREATE TABLE cac_v1.event_log_y2024m03 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-03-01') TO ('2024-04-01'); + +CREATE TABLE cac_v1.event_log_y2024m04 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-04-01') TO ('2024-05-01'); + +CREATE TABLE cac_v1.event_log_y2024m05 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-05-01') TO ('2024-06-01'); + +CREATE TABLE cac_v1.event_log_y2024m06 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-06-01') TO ('2024-07-01'); + +CREATE TABLE cac_v1.event_log_y2024m07 PARTITION OF cac_v1.event_log FOR +VALUES +FROM ('2024-07-01') TO ('2024-08-01'); diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 992f759d9..f81be41e0 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -44,9 +44,191 @@ pub mod cac_v1 { } } + diesel::table! { + cac_v1.event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, dimensions, + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, ); } diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql index cde08addb..e0f8b9396 100644 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql +++ b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql @@ -1,3 +1,3 @@ -- This file should undo anything in `up.sql` -DROP TABLE experiments; -DROP TYPE experiment_status_type; +DROP TABLE cac_v1.experiments; +DROP TYPE cac_v1.experiment_status_type; diff --git a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql new file mode 100644 index 000000000..61c1ea1f9 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TRIGGER experiments_audit ON cac_v1.experiments; + diff --git a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql new file mode 100644 index 000000000..c417a0786 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +CREATE TRIGGER experiments_audit +AFTER INSERT OR UPDATE OR DELETE ON cac_v1.experiments +FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 819943942..c04704988 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -48,6 +48,175 @@ pub mod cac_v1 { } } + diesel::table! { + cac_v1.event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + + diesel::table! { + cac_v1.event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } + } + diesel::table! { use diesel::sql_types::*; use super::sql_types::ExperimentStatusType; @@ -71,6 +240,19 @@ pub mod cac_v1 { contexts, default_configs, dimensions, + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, experiments, ); } diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index 155a6b6af..0fc396d2f 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -1,5 +1,5 @@ diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs -index ef6ee2d..8199439 100644 +index 65aa8b6..722a39a 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -9,10 +9,6 @@ pub mod cac_v1 { From 4f7e0e2a163782d12270dec58f2e44a019e26c0c Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 10 Aug 2023 15:55:37 +0530 Subject: [PATCH 083/352] fix: using audit log tstamp for checking last-modified --- .../src/api/config/handlers.rs | 10 ++-- .../src/api/experiments/handlers.rs | 52 +++++++++++++++---- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index e89b185f3..8c3493fe8 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,9 +1,10 @@ use super::types::Config; use crate::db::schema::cac_v1::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, + event_log::dsl as event_log, }; use actix_web::{get, web::Data, HttpRequest, HttpResponse, Scope}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value, Value::Null}; use service_utils::{helpers::ToActixErr, service::types::AppState}; @@ -18,8 +19,9 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result> = ctxt::contexts - .select(max(ctxt::created_at)) + let max_created_at: Option = event_log::event_log + .select(max(event_log::timestamp)) + .filter(event_log::table_name.eq("contexts")) .first(&mut conn) .map_err_to_internal_server("error getting created at", Null)?; @@ -29,7 +31,7 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result Scope { @@ -287,20 +288,51 @@ async fn conclude( #[get("")] async fn list_experiments( + req: HttpRequest, filters: Query, db_conn: DbConnection, -) -> actix_web::Result, AppError> { +) -> actix_web::Result { let DbConnection(mut conn) = db_conn; - use crate::db::schema::cac_v1::experiments::dsl::*; + + let max_event_timestamp: Option = event_log::event_log + .filter(event_log::table_name.eq("experiments")) + .select(diesel::dsl::max(event_log::timestamp)) + .first(&mut conn) + .map_err(|e| { + println!("error fetching max_event_timestamp: {e}"); + AppError { + message: String::from("Something went wrong"), + possible_fix: String::from("Please try again later"), + status_code: StatusCode::INTERNAL_SERVER_ERROR, + } + })?; + + let last_modified = req + .headers() + .get("If-Modified-Since") + .and_then(|header_val| header_val.to_str().ok()) + .and_then(|header_str| { + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) + .ok() + }); + + if max_event_timestamp.is_some() && max_event_timestamp < last_modified { + return Ok(HttpResponse::NotModified().finish()); + }; + let query_builder = |filters: &ListFilters| { - let mut builder = experiments.into_boxed(); + let mut builder = experiments::experiments.into_boxed(); if let Some(states) = filters.status.clone() { - builder = builder.filter(status.eq_any(states.0.clone())); + builder = builder.filter(experiments::status.eq_any(states.0.clone())); } let now = Utc::now(); builder - .filter(last_modified.ge(filters.from_date.unwrap_or(now - Duration::hours(24)))) - .filter(last_modified.le(filters.to_date.unwrap_or(now))) + .filter( + experiments::last_modified + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(experiments::last_modified.le(filters.to_date.unwrap_or(now))) }; let filters = filters.into_inner(); let base_query = query_builder(&filters); @@ -309,7 +341,7 @@ async fn list_experiments( let limit = filters.count.unwrap_or(10); let offset = (filters.page.unwrap_or(1) - 1) * limit; let query = base_query - .order(last_modified.desc()) + .order(experiments::last_modified.desc()) .limit(limit) .offset(offset); @@ -335,7 +367,7 @@ async fn list_experiments( match db_result { Ok(response) => { - return Ok(Json(ExperimentsResponse { + return Ok(HttpResponse::Ok().json(ExperimentsResponse { total_items: number_of_experiments, total_pages: total_pages, data: response From b98528652a1243064a96d9fbd8ce6a32a60588a7 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 16 Aug 2023 19:17:49 +0530 Subject: [PATCH 084/352] ci: udpated `docker container ls` filter --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index 23506e5a2..2628e954c 100644 --- a/makefile +++ b/makefile @@ -49,7 +49,7 @@ run: make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: - -docker rm -f $$(docker container ls --filter name=^context-aware-config -q) + -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) npm ci --loglevel=error make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ From 7caaabc9f8703fbbbc6ccfc906db385888e1ceae Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 16 Aug 2023 16:40:43 +0530 Subject: [PATCH 085/352] feat: added support for CUG in super position client --- crates/superposition_client/src/lib.rs | 48 ++++++++++++------- crates/superposition_client/src/types.rs | 8 ++++ .../src/main.rs | 4 +- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index cf4d8b113..d4dfe3bd7 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -8,7 +8,10 @@ use tokio::{ time::{self, Duration}, }; pub use types::{Config, Variants}; -use types::{ExperimentStore, Experiments, ListExperimentsResponse, Variant}; +use types::{ + ExperimentStatusType, ExperimentStore, Experiments, ListExperimentsResponse, Variant, + VariantType, +}; #[derive(Clone, Debug)] pub struct Client { @@ -57,9 +60,7 @@ impl Client { types::ExperimentStatusType::CONCLUDED => { exp_store.remove(&exp_id) } - types::ExperimentStatusType::INPROGRESS => { - exp_store.insert(exp_id, experiment) - } + _ => exp_store.insert(exp_id, experiment), }; } } // write lock on exp store releases here @@ -68,7 +69,7 @@ impl Client { } } - pub async fn get_applicable_variant(&self, context: &Value, toss: u8) -> Vec { + pub async fn get_applicable_variant(&self, context: &Value, toss: i8) -> Vec { let running_experiments = self.experiments.read().await; // try and if json logic works let mut experiments: Experiments = Vec::new(); @@ -81,9 +82,12 @@ impl Client { let mut variants: Vec = Vec::new(); for exp in experiments { - if let Some(v) = - self.decide_variant(exp.traffic_percentage, exp.variants, toss) - { + if let Some(v) = self.decide_variant( + exp.traffic_percentage, + exp.variants, + toss, + exp.status, + ) { variants.push(v.id) } } @@ -100,19 +104,29 @@ impl Client { fn decide_variant( &self, traffic: u8, - applicable_vars: Variants, - toss: u8, + applicable_variants: Variants, + toss: i8, + status: ExperimentStatusType, ) -> Option { - let variant_count = applicable_vars.len() as u8; - let range = (traffic * variant_count) as u32; - if (toss as u32) >= range { + if toss < 0 && status != ExperimentStatusType::CREATED { + return None; + } else if toss < 0 { + for variant in applicable_variants.iter() { + if variant.variant_type == VariantType::EXPERIMENTAL { + return Some(variant.clone()); + } + } + } + let variant_count = applicable_variants.len() as u8; + let range = (traffic * variant_count) as i32; + if (toss as i32) >= range { return None; } let buckets = (1..=variant_count) - .map(|i| traffic * i) - .collect::>(); + .map(|i| (traffic * i) as i8) + .collect::>(); let index = buckets.into_iter().position(|x| toss < x); - applicable_vars.get(index.unwrap()).map(|x| x.clone()) + applicable_variants.get(index.unwrap()).map(Variant::clone) } } @@ -130,7 +144,7 @@ async fn get_experiments( "{hostname}/experiments?from_date={start_date}&to_date={now}&page={page}&count={requesting_count}" ); let list_experiments_response = http_client - .get(format!("{endpoint}&status=INPROGRESS,CONCLUDED")) + .get(format!("{endpoint}&status=CREATED,INPROGRESS,CONCLUDED")) .send() .await .unwrap() diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index f11e9fa7d..ec5c231b2 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -11,14 +11,22 @@ pub struct Config { #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] pub(crate) enum ExperimentStatusType { + CREATED, INPROGRESS, CONCLUDED, } +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub(crate) enum VariantType { + CONTROL, + EXPERIMENTAL, +} + #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Variant { pub id: String, pub overrides: Value, + pub(crate) variant_type: VariantType, } pub type Variants = Vec; diff --git a/crates/superposition_client_integration_example/src/main.rs b/crates/superposition_client_integration_example/src/main.rs index 3a50d25f3..230029d8d 100644 --- a/crates/superposition_client_integration_example/src/main.rs +++ b/crates/superposition_client_integration_example/src/main.rs @@ -24,7 +24,7 @@ async fn main() -> std::io::Result<()> { ) .service(get_variants) }) - .bind(("127.0.0.1", 8082))? + .bind(("127.0.0.1", 8083))? .run() .await } @@ -32,7 +32,7 @@ async fn main() -> std::io::Result<()> { #[get("/variants/{client_id}/{platform}/{toss}")] async fn get_variants( state: Data, - path: Path<(String, String, u8)>, + path: Path<(String, String, i8)>, ) -> HttpResponse { let (client_id, platform, toss) = path.into_inner(); println!("client state on the server = {:?}", state); From c702beb0ca4fb7f83ebae8458deac092e6552ada Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Thu, 10 Aug 2023 12:32:25 +0530 Subject: [PATCH 086/352] - Unique id validation for experiments --- .../src/api/experiments/handlers.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index e1fe3a2b1..953b15bbd 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use actix_web::{ get, http::StatusCode, @@ -49,6 +51,18 @@ async fn create( let override_keys = &req.override_keys; let mut variants = req.variants.to_vec(); + let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter( + variants + .iter() + .map(|v| v.id.as_str()) + ); + + if unique_ids_of_variants_from_req.len() != variants.len() { + return Err(actix_web::error::ErrorBadRequest( + "variant ids are expected to be unique".to_string(), + )); + } + // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant check_variant_types(&variants) From 131f37cdf05507018aa3861ec7bf7efde46d0cf6 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Mon, 14 Aug 2023 15:26:13 +0530 Subject: [PATCH 087/352] ci: fix newman dev dependency ref --- package-lock.json | 7 +++---- package.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d4b76479..29ad7d57e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "context-aware-configuration", "version": "0.0.1", "devDependencies": { - "newmandir": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" + "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" } }, "node_modules/@postman/form-data": { @@ -781,10 +781,9 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/newmandir": { - "name": "newman", + "node_modules/newman": { "version": "5.3.2", - "resolved": "git+ssh://git@github.com/knutties/newman.git#5a27c2ea3e80c21d8bfabdebbdec48e8ed5d69fd", + "resolved": "git+ssh://git@github.com/knutties/newman.git#7103847a473275d2561406ba926defb8eab0aeb7", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2802d40a3..8c89dc736 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,6 @@ "test": "./node_modules/.bin/newman dir-run postman/cac" }, "devDependencies": { - "newmandir": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" + "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" } } From 9ddb39e092ce04829fe14ac0de321238f9ee2d63 Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Mon, 14 Aug 2023 14:35:28 +0530 Subject: [PATCH 088/352] readme: added steps for repo setup --- README.md | 58 ++++++++++++++++++++++++++++++++----------------------- flake.nix | 1 + 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1f894454a..361e1fde9 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,39 @@ # Context Aware Config # -This README would normally document whatever steps are necessary to get your application up and running. - -### What is this repository for? ### - -* Quick summary -* Version -* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo) ### How do I get set up? ### -* Summary of set up -* Configuration -* Dependencies -* Database configuration -* How to run tests -* Deployment instructions - -### Contribution guidelines ### - -* Writing tests -* Code review -* Other guidelines - -### Who do I talk to? ### - -* Repo owner or admin -* Other community or team contact \ No newline at end of file +* Clone this repo +* [Install nix](https://zero-to-nix.com/start/install) +* Start docker daemon either through docker desktop or using the command given below + ```bash + $ open --background -a Docker + ``` +* Make sure your brew's postgres service is not running + + ```bash + # Check if service is running + $ brew services + # If postgres is running, stop it via brew + $ brew services stop postgresql@ + $ cd context-aware-config + ``` +* cd to the repo dir + ```bash + $ cd context-aware-config + ``` +* run these commands + ```bash + $ nix develop + $ make run + ``` +* If you get something like this in the output, you are good to go + ```bash + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.291Z","value":"starting 5 workers"} + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.292Z","value":"Actix runtime found; starting in Actix runtime"} + ``` +* You can hit the `/health` endpoint to confirm server is ready to serve + ```bash + $ curl --location 'http://localhost:8080/health' + #Expected Response : "Health is good :D" + ``` diff --git a/flake.nix b/flake.nix index a3f3e2bf8..6d9700182 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,7 @@ stdenv.cc pkgconfig awscli + jq ]; darwinPkgs = with pkgs; [ darwin.apple_sdk.frameworks.Security From 9cebcae82fc5308cfbcc69b324d62476ffe245f3 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 16 Aug 2023 21:15:21 +0530 Subject: [PATCH 089/352] fix: sorting same priority contexts with created_at --- crates/context-aware-config/src/api/config/handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 8c3493fe8..88d952b18 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -41,7 +41,7 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result(&mut conn) .map_err_to_internal_server("error getting contexts", Null)?; From f1b420d628156b2dca4d78139d0a4cccb38c8181 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 23 Aug 2023 13:41:50 +0530 Subject: [PATCH 090/352] fix: allowing cug users to fall under test variants currently in case of non-experimentation releases when a user does CUG he is able to fall under the stagered spec, same should happen for experimentation as well --- crates/superposition_client/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index d4dfe3bd7..43a6a0b04 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -108,9 +108,7 @@ impl Client { toss: i8, status: ExperimentStatusType, ) -> Option { - if toss < 0 && status != ExperimentStatusType::CREATED { - return None; - } else if toss < 0 { + if toss < 0 { for variant in applicable_variants.iter() { if variant.variant_type == VariantType::EXPERIMENTAL { return Some(variant.clone()); From fd0c879545b4a5cdbb7d0fd84263ec0048e52b93 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Tue, 22 Aug 2023 17:25:49 +0530 Subject: [PATCH 091/352] - Resolve config based on query params and priority --- Cargo.lock | 3 +- crates/cac_client/Cargo.toml | 2 +- crates/cac_client/src/eval.rs | 19 +++- crates/cac_client/src/lib.rs | 6 +- crates/context-aware-config/Cargo.toml | 2 + .../src/api/config/handlers.rs | 106 ++++++++++++++---- crates/service-utils/src/service/types.rs | 2 +- 7 files changed, 111 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e07f5b1f9..2a73286d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,7 +561,7 @@ dependencies = [ ] [[package]] -name = "cac" +name = "cac_client" version = "0.1.0" dependencies = [ "actix-web", @@ -692,6 +692,7 @@ dependencies = [ "base64 0.21.2", "blake3", "bytes 1.4.0", + "cac_client", "chrono", "derive_more", "diesel", diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 979017c64..685354f2e 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cac" +name = "cac_client" version = "0.1.0" edition = "2021" diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index 8ef68b4be..43f883cdb 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -114,16 +114,27 @@ fn get_overrides( Ok(required_overrides) } +pub fn eval_cac( + default_config: Value, + contexts: Vec, + overrides: Map, + query_data: &Value, +) -> serde_json::Result { + // get_overrides runs json logic and gives patch_entries from overrides.json + let overrides = get_overrides(query_data, contexts, overrides, true)?; + + // patch_entries received from overides after json logic are then merged over default_config + deep_merge(&default_config, &overrides) +} + pub fn get_mjos_override( query_data: Value, contexts: Vec, overrides: Map, default_config: Value, ) -> serde_json::Result { - // get_overrides runs json logic and gives patch_entries from overrides.json - let overrides = get_overrides(&query_data, contexts, overrides, true)?; - // patch_entries received from overides after json logic are then merged over default_config - let overrides_config = deep_merge(&default_config, &overrides)?; + + let overrides_config = eval_cac(default_config, contexts, overrides, &query_data)?; if Some(&json!([])) == overrides_config.get("patch_entries") { return Err(de::Error::custom("No patches found")); diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index a623f57dd..01edbf2cb 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -18,8 +18,8 @@ use serde_json::{Map, Value, json}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { - condition: Value, - override_with_keys: [String; 1], + pub condition: Value, + pub override_with_keys: [String; 1], } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -120,3 +120,5 @@ impl Client { get_mjos_override(query_data, cac.contexts.clone(), cac.overrides.clone(), json!(cac.default_configs)).map_err_to_string() } } + +pub use eval::eval_cac; diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 62b154b44..1edf0a1df 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +cac_client = { path = "../cac_client" } + # env dotenv = "0.15.0" # Https server framework diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 88d952b18..9f89cda4f 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,28 +1,29 @@ +use std::collections::HashMap; + use super::types::Config; use crate::db::schema::cac_v1::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; -use actix_web::{get, web::Data, HttpRequest, HttpResponse, Scope}; +use actix_web::{error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope}; use chrono::{DateTime, NaiveDateTime, Utc}; -use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl}; -use serde_json::{Map, Value, Value::Null}; -use service_utils::{helpers::ToActixErr, service::types::AppState}; +use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl, r2d2::{PooledConnection, ConnectionManager}, PgConnection}; +use serde_json::{json, Map, Value, Value::Null}; +use service_utils::{helpers::ToActixErr, service::types::DbConnection}; +use cac_client::eval_cac; pub fn endpoints() -> Scope { - Scope::new("").service(get) + Scope::new("") + .service(get) + .service(get_resolved_config) } -#[get("")] -async fn get(req: HttpRequest, state: Data) -> actix_web::Result { - let mut conn = state - .db_pool - .get() - .map_err_to_internal_server("error getting a connection from db pool", Null)?; + +fn is_not_modified (req: &HttpRequest, conn: &mut PooledConnection>) -> actix_web::Result { let max_created_at: Option = event_log::event_log .select(max(event_log::timestamp)) .filter(event_log::table_name.eq("contexts")) - .first(&mut conn) + .first(conn) .map_err_to_internal_server("error getting created at", Null)?; let last_modified = req @@ -35,14 +36,15 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result>) -> actix_web::Result { let contexts_vec = ctxt::contexts .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) .order_by((ctxt::priority.asc(), ctxt::created_at.asc())) - .load::<(String, Value, String, Value)>(&mut conn) + .load::<(String, Value, String, Value)>(conn) .map_err_to_internal_server("error getting contexts", Null)?; let (contexts, overrides) = contexts_vec.into_iter().fold( @@ -61,7 +63,7 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result(&mut conn) + .load::<(String, Value)>(conn) .map_err_to_internal_server("error getting default configs", Null)?; let default_configs = @@ -72,11 +74,75 @@ async fn get(req: HttpRequest, state: Data) -> actix_web::Result actix_web::Result { + + let DbConnection(mut conn) = db_conn; + + if is_not_modified(&req, &mut conn)? { + return Ok(HttpResponse::NotModified().finish()); + } + + Ok( + HttpResponse::Ok() + .json( + generate_cac(&mut conn) + .await? + ) + ) +} + +#[get("/resolve")] +async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result { + + let DbConnection(mut conn) = db_conn; + let params = + Query::>::from_query(req.query_string()) + .map_err(|_| ErrorBadRequest("error getting query params"))?; + + let mut query_params_map = Map::new(); + + for item in params.0.into_iter() { + query_params_map.insert( + item.0, + item.1 + .parse::() + .map_or_else( + |_| json!(item.1), + |int_val| json!(int_val) + ) + ); + } + + if is_not_modified(&req, &mut conn)? { + return Ok(HttpResponse::NotModified().finish()); + } + + let res = generate_cac(&mut conn).await?; + + let cac_client_contexts = + res.contexts.into_iter().map(|val| cac_client::Context { + condition: val.condition, + override_with_keys: val.override_with_keys + }).collect(); + - return Ok(HttpResponse::Ok().json(config)); + Ok(HttpResponse::Ok() + .json( + eval_cac( + json!(res.default_configs), + cac_client_contexts, + res.overrides, + &json!(query_params_map), + ).map_err_to_internal_server("cac eval failed", Null)? + ) + ) } diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index b0a1b4819..229fc0005 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -53,7 +53,7 @@ impl FromRequest for AuthenticationInfo { .get(1) .map(|token| token.to_string()) }); - dbg!(format!("Token is \"{:?}\"", opt_token)); + let opt_admin_token = req .app_data() .map(|d: &Data| d.admin_token.as_str()); From 42b9d773cce0128ac47791a6bdfc6a6a3a1bf086 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Tue, 29 Aug 2023 14:33:02 +0530 Subject: [PATCH 092/352] test: fix newman version used in tests --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 29ad7d57e..5f3f53158 100644 --- a/package-lock.json +++ b/package-lock.json @@ -590,9 +590,9 @@ } }, "node_modules/httpreq": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.5.2.tgz", - "integrity": "sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.0.tgz", + "integrity": "sha512-P2ROAc2JG4Y1+EL8vRusYb3p6BK5WRZLVj8WLrvtQJL9qQgocqieU9+0cWWwrL2FroUjXGtUDo4lOKXoq+Et+g==", "dev": true, "engines": { "node": ">= 6.15.1" @@ -783,7 +783,7 @@ }, "node_modules/newman": { "version": "5.3.2", - "resolved": "git+ssh://git@github.com/knutties/newman.git#7103847a473275d2561406ba926defb8eab0aeb7", + "resolved": "git+ssh://git@github.com/knutties/newman.git#4f7c11d69e61b3d1797ff2c56f2e87ddf375e442", "dev": true, "license": "Apache-2.0", "dependencies": { From 4b8fdfc49526e7dd5a4349e0ae27ca7e4194b095 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 3 Aug 2023 19:34:45 +0530 Subject: [PATCH 093/352] ci: added postman collection for experimentation-platform --- package.json | 2 +- postman/cac.postman_collection.json | 1423 +++++++++-------- postman/cac/.info.json | 2 +- postman/cac/.meta.json | 4 +- postman/cac/.variable.json | 2 +- .../Context/Create Context/.event.meta.json | 5 + .../cac/Context/Create Context/request.json | 21 +- .../Context/Delete Context/.event.meta.json | 5 + .../cac/Context/Get Context/.event.meta.json | 5 + .../cac/Context/List Context/.event.meta.json | 5 + .../cac/Context/Move Context/.event.meta.json | 5 + postman/cac/Context/Move Context/request.json | 21 +- .../Context/Update Context/.event.meta.json | 5 + .../cac/Context/Update Context/request.json | 21 +- .../Add default-config key/.event.meta.json | 5 + .../Add default-config key/request.json | 15 +- .../Create Dimension/.event.meta.json | 5 + .../Dimension/Create Dimension/request.json | 13 +- .../cac/config/Get Config/.event.meta.json | 5 + postman/cac/config/Get Config/request.json | 2 +- ...latform-local-env.postman_environment.json | 21 + ...mentation-platform.postman_collection.json | 807 ++++++++++ postman/experimentation-platform/.event.json | 22 + postman/experimentation-platform/.info.json | 7 + postman/experimentation-platform/.meta.json | 11 + .../experimentation-platform/.variable.json | 14 + .../Conclude/.event.meta.json | 5 + .../Conclude/event.test.js | 97 ++ .../Conclude/request.json | 37 + .../Conclude/response.json | 1 + .../Create Experiment/.event.meta.json | 6 + .../Create Experiment/event.prerequest.js | 77 + .../Create Experiment/event.test.js | 219 +++ .../Create Experiment/request.json | 78 + .../Create Experiment/response.json | 1 + .../Get Experiment/.event.meta.json | 5 + .../Get Experiment/event.test.js | 3 + .../Get Experiment/request.json | 14 + .../Get Experiment/response.json | 1 + .../.event.meta.json | 5 + .../event.test.js | 3 + .../request.json | 37 + .../response.json | 1 + .../.event.meta.json | 5 + .../event.test.js | 3 + .../request.json | 37 + .../response.json | 1 + .../request.json | 40 + .../response.json | 1 + .../Ramp/.event.meta.json | 5 + .../Ramp/event.test.js | 37 + .../Ramp/request.json | 37 + .../Ramp/response.json | 1 + 53 files changed, 2498 insertions(+), 712 deletions(-) create mode 100644 postman/cac/Context/Create Context/.event.meta.json create mode 100644 postman/cac/Context/Delete Context/.event.meta.json create mode 100644 postman/cac/Context/Get Context/.event.meta.json create mode 100644 postman/cac/Context/List Context/.event.meta.json create mode 100644 postman/cac/Context/Move Context/.event.meta.json create mode 100644 postman/cac/Context/Update Context/.event.meta.json create mode 100644 postman/cac/Default Config/Add default-config key/.event.meta.json create mode 100644 postman/cac/Dimension/Create Dimension/.event.meta.json create mode 100644 postman/cac/config/Get Config/.event.meta.json create mode 100644 postman/experiment-platform-local-env.postman_environment.json create mode 100644 postman/experimentation-platform.postman_collection.json create mode 100644 postman/experimentation-platform/.event.json create mode 100644 postman/experimentation-platform/.info.json create mode 100644 postman/experimentation-platform/.meta.json create mode 100644 postman/experimentation-platform/.variable.json create mode 100644 postman/experimentation-platform/Conclude/.event.meta.json create mode 100644 postman/experimentation-platform/Conclude/event.test.js create mode 100644 postman/experimentation-platform/Conclude/request.json create mode 100644 postman/experimentation-platform/Conclude/response.json create mode 100644 postman/experimentation-platform/Create Experiment/.event.meta.json create mode 100644 postman/experimentation-platform/Create Experiment/event.prerequest.js create mode 100644 postman/experimentation-platform/Create Experiment/event.test.js create mode 100644 postman/experimentation-platform/Create Experiment/request.json create mode 100644 postman/experimentation-platform/Create Experiment/response.json create mode 100644 postman/experimentation-platform/Get Experiment/.event.meta.json create mode 100644 postman/experimentation-platform/Get Experiment/event.test.js create mode 100644 postman/experimentation-platform/Get Experiment/request.json create mode 100644 postman/experimentation-platform/Get Experiment/response.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json create mode 100644 postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json create mode 100644 postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json create mode 100644 postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json create mode 100644 postman/experimentation-platform/Ramp/.event.meta.json create mode 100644 postman/experimentation-platform/Ramp/event.test.js create mode 100644 postman/experimentation-platform/Ramp/request.json create mode 100644 postman/experimentation-platform/Ramp/response.json diff --git a/package.json b/package.json index 8c89dc736..9507e287d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "description": "This is just to run automated newman tests for this service", "scripts": { - "test": "./node_modules/.bin/newman dir-run postman/cac" + "test": "./node_modules/.bin/newman dir-run ./postman/cac && ./node_modules/.bin/newman dir-run -e postman/experiment-platform-local-env.postman_environment.json ./postman/experimentation-platform " }, "devDependencies": { "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json index 7cf1d77ae..c34753042 100644 --- a/postman/cac.postman_collection.json +++ b/postman/cac.postman_collection.json @@ -1,759 +1,790 @@ { - "collection": { - "item": [ - { - "name": "config", - "item": [ - { - "name": "Get Config", - "event": [ + "item": [ + { + "name": "config", + "item": [ + { + "name": "Get Config", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + " let response = pm.response.json();", + " let expected_response = {", + " \"contexts\": [],", + " \"overrides\": {},", + " \"default_configs\": {}", + " };", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response));", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" } ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" ], - "url": { - "raw": "{{host}}/config", - "host": [ - "{{host}}" + "path": [ + "config" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default Config", + "item": [ + { + "name": "Add default-config key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(key, value) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const default_configs = resp_obj.default_configs;", + "", + " console.log(`Checking if key=${key} with value=${value} in default_configs`);", + " pm.expect(default_configs[key]).to.be.eq(value);", + " });", + "}", + "", + "pm.test(\"201 check\", function () {", + " pm.response.to.have.status(201);", + "})", + "", + "pm.test(\"Check if key added to default config\", function () {", + " const key = \"key1\", value = \"value1\";", + " getConfigAndTest(key, value);", + "});", + "" ], - "path": [ - "config" - ] + "type": "text/javascript" } - }, - "response": [] - } - ] - }, - { - "name": "Default Config", - "item": [ - { - "name": "Add default-config key", - "event": [ + } + ], + "request": { + "method": "PUT", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(key, value) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const default_configs = resp_obj.default_configs;", - "", - " console.log(`Checking if key=${key} with value=${value} in default_configs`);", - " pm.expect(default_configs[key]).to.be.eq(value);", - " });", - "}", - "", - "pm.test(\"201 check\", function () {", - " pm.response.to.have.status(201);", - "})", - "", - "pm.test(\"Check if key added to default config\", function () {", - " const key = \"key1\", value = \"value1\";", - " getConfigAndTest(key, value);", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"value\": \"value1\",\n \"schema\": {\n \"type\": \"string\",\n \"pattern\": \".*\"\n }\n}", - "options": { - "raw": { - "language": "json" - } + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } }, - "url": { - "raw": "{{host}}/default-config/key1", - "host": [ - "{{host}}" + "raw": "{\"value\":\"value1\",\"schema\":{\"type\":\"string\",\"pattern\":\".*\"}}" + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Dimension", + "item": [ + { + "name": "Create Dimension", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"201 Check\", function () {", + " pm.response.to.have.status(201);", + "})" ], - "path": [ - "default-config", - "key1" - ] + "type": "text/javascript" } - }, - "response": [] - } - ] - }, - { - "name": "Dimension", - "item": [ - { - "name": "Create Dimension", - "event": [ + } + ], + "request": { + "method": "PUT", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"201 Check\", function () {", - " pm.response.to.have.status(201);", - "})" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"dimension\": \"clientId\",\n \"priority\": 100,\n \"type\": \"STRING\"\n}", - "options": { - "raw": { - "language": "json" - } + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } }, - "url": { - "raw": "{{host}}/dimension", - "host": [ - "{{host}}" + "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"type\":\"STRING\"}" + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Context", + "item": [ + { + "name": "Create Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" ], - "path": [ - "dimension" - ] + "type": "text/javascript" } - }, - "response": [] - } - ] - }, - { - "name": "Context", - "item": [ - { - "name": "Create Context", - "event": [ + } + ], + "request": { + "method": "PUT", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - " ", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id]; ", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"piyaz\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value2\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } }, - "url": { - "raw": "{{host}}/context", - "host": [ - "{{host}}" - ], - "path": [ - "context" - ] - } + "raw": "{\"override\":{\"key1\":\"value2\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}" }, - "response": [] + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } }, - { - "name": "Update Context", - "event": [ + "response": [] + }, + { + "name": "Update Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - " ", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id]; ", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"piyaz\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } }, - "url": { - "raw": "{{host}}/context", - "host": [ - "{{host}}" - ], - "path": [ - "context" - ] - } + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}" }, - "response": [] + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } }, - { - "name": "Move Context", - "event": [ + "response": [] + }, + { + "name": "Move Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", + " throw \"old context not removed on move\"", + " }", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + "", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id];", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", + " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", - " throw \"old context not removed on move\"", - " }", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - "", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id];", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", - " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"tamatar\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"tamatar\"\n ]\n }\n}", - "options": { - "raw": { - "language": "json" - } + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } }, - "url": { - "raw": "{{host}}/context/move/{{context_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "move", - "{{context_id}}" - ] - } + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"tamatar\"]}}" }, - "response": [] + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } }, - { - "name": "Get Context", - "event": [ + "response": [] + }, + { + "name": "Get Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const expected_context = {", + " \"id\": pm.environment.get(\"context_id\"),", + " \"value\": {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " },", + " \"override_id\": pm.environment.get(\"override_id\"),", + " \"priority\": 100,", + " \"override\": {", + " \"key1\": \"value3\"", + " }", + "};", + "", + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Context equality check\", function() {", + " const response = pm.response.json();", + " ", + " delete response.created_at;", + " delete response.created_by;", + "", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const expected_context = {", - " \"id\": pm.environment.get(\"context_id\"),", - " \"value\": {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"tamatar\"", - " ]", - " },", - " \"override_id\": pm.environment.get(\"override_id\"),", - " \"priority\": 100,", - " \"override\": {", - " \"key1\": \"value3\"", - " }", - "};", - "", - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Context equality check\", function() {", - " const response = pm.response.json();", - " ", - " delete response.created_at;", - " delete response.created_by;", - "", - " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" } ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" ], - "url": { - "raw": "{{host}}/context/{{context_id}}", - "host": [ - "{{host}}" + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "List Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "", + "pm.test(\"Response validation\", function() {", + " const response = pm.response.json();", + " if (response.length == 0) {", + " throw \"list context should return at least one context now\"", + " }", + "});", + "" ], - "path": [ - "context", - "{{context_id}}" - ] + "type": "text/javascript" } - }, - "response": [] - }, - { - "name": "List Context", - "event": [ + } + ], + "request": { + "method": "GET", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})", - "", - "", - "pm.test(\"Response validation\", function() {", - " const response = pm.response.json();", - " if (response.length == 0) {", - " throw \"list context should return at least one context now\"", - " }", - "});", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" } ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" ], - "url": { - "raw": "{{host}}/context/list", - "host": [ - "{{host}}" + "path": [ + "context", + "list" + ] + } + }, + "response": [] + }, + { + "name": "Delete Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "const context_id = pm.environment.get(\"context_id\");", + "", + "pm.test(\"204 check\", function() {", + " pm.response.to.have.status(204);", + "})", + "", + "pm.test(\"Fetch for context should fail with 404\", function () {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " console.log(`alloo ${error}`);", + " throw error;", + " }", + "", + " pm.expect(response.code).to.be.eq(404);", + " });", + "})", + "" ], - "path": [ - "context", - "list" - ] + "type": "text/javascript" } - }, - "response": [] - }, - { - "name": "Delete Context", - "event": [ + } + ], + "request": { + "method": "DELETE", + "header": [ { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "const context_id = pm.environment.get(\"context_id\");", - "", - "pm.test(\"204 check\", function() {", - " pm.response.to.have.status(204);", - "})", - "", - "pm.test(\"Fetch for context should fail with 404\", function () {", - " const getRequest = {", - " url: `${host}/context/${context_id}`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " console.log(`alloo ${error}`);", - " throw error;", - " }", - "", - " pm.expect(response.code).to.be.eq(404);", - " });", - "})", - "" - ], - "type": "text/javascript" - } + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" } ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" ], - "url": { - "raw": "{{host}}/context/{{context_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "{{context_id}}" - ] - } - }, - "response": [] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] } - ], - "info": { - "_postman_id": "c0e007df-4ea8-478e-a47b-74194b94887f", - "name": "cac", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, - "variable": [ - { - "key": "host", - "value": "http://localhost:8080", - "type": "default" - }, - { - "key": "token", - "value": "12345678", - "type": "default" + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] } - ] - } + } + ], + "info": { + "_postman_id": "9d2f7da8-68e5-4f9a-b5a8-9abd8f6c8cf0", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] } diff --git a/postman/cac/.info.json b/postman/cac/.info.json index 1c2a232e6..6212fbc7f 100644 --- a/postman/cac/.info.json +++ b/postman/cac/.info.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "c0e007df-4ea8-478e-a47b-74194b94887f", + "_postman_id": "9d2f7da8-68e5-4f9a-b5a8-9abd8f6c8cf0", "name": "cac", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" } diff --git a/postman/cac/.meta.json b/postman/cac/.meta.json index b6ff22778..5b8cd24d0 100644 --- a/postman/cac/.meta.json +++ b/postman/cac/.meta.json @@ -1,8 +1,8 @@ { "childrenOrder": [ + "config", "Default Config", "Dimension", - "Context", - "Config" + "Context" ] } diff --git a/postman/cac/.variable.json b/postman/cac/.variable.json index 191e8214a..5460e39fb 100644 --- a/postman/cac/.variable.json +++ b/postman/cac/.variable.json @@ -2,7 +2,7 @@ "variable": [ { "key": "host", - "value": "http://127.0.0.1:8080", + "value": "http://localhost:8080", "type": "default" }, { diff --git a/postman/cac/Context/Create Context/.event.meta.json b/postman/cac/Context/Create Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Create Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Create Context/request.json b/postman/cac/Context/Create Context/request.json index 8d3c070bf..2dd61b9ce 100644 --- a/postman/cac/Context/Create Context/request.json +++ b/postman/cac/Context/Create Context/request.json @@ -4,16 +4,33 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value2\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", "options": { "raw": { "language": "json" } + }, + "raw_json_formatted": { + "override": { + "key1": "value2" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + } } }, "url": { diff --git a/postman/cac/Context/Delete Context/.event.meta.json b/postman/cac/Context/Delete Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Delete Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Get Context/.event.meta.json b/postman/cac/Context/Get Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Get Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/List Context/.event.meta.json b/postman/cac/Context/List Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/List Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Move Context/.event.meta.json b/postman/cac/Context/Move Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Move Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Move Context/request.json b/postman/cac/Context/Move Context/request.json index 8e6f3ee6f..a6eabcb79 100644 --- a/postman/cac/Context/Move Context/request.json +++ b/postman/cac/Context/Move Context/request.json @@ -4,16 +4,33 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"tamatar\"\n ]\n }\n}", "options": { "raw": { "language": "json" } + }, + "raw_json_formatted": { + "override": { + "key1": "value3" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + } } }, "url": { diff --git a/postman/cac/Context/Update Context/.event.meta.json b/postman/cac/Context/Update Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Update Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Update Context/request.json b/postman/cac/Context/Update Context/request.json index 547daecfc..7e3588e57 100644 --- a/postman/cac/Context/Update Context/request.json +++ b/postman/cac/Context/Update Context/request.json @@ -4,16 +4,33 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"override\": {\n \"key1\": \"value3\"\n },\n \"context\": {\n \"==\": [\n {\n \"var\": \"clientId\"\n },\n \"piyaz\"\n ]\n }\n}", "options": { "raw": { "language": "json" } + }, + "raw_json_formatted": { + "override": { + "key1": "value3" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + } } }, "url": { diff --git a/postman/cac/Default Config/Add default-config key/.event.meta.json b/postman/cac/Default Config/Add default-config key/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Default Config/Add default-config key/request.json b/postman/cac/Default Config/Add default-config key/request.json index a289a6aa7..de3a62cd9 100644 --- a/postman/cac/Default Config/Add default-config key/request.json +++ b/postman/cac/Default Config/Add default-config key/request.json @@ -4,16 +4,27 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"value\": \"value1\",\n \"schema\": {\n \"type\": \"string\",\n \"pattern\": \".*\"\n }\n}", "options": { "raw": { "language": "json" } + }, + "raw_json_formatted": { + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } } }, "url": { diff --git a/postman/cac/Dimension/Create Dimension/.event.meta.json b/postman/cac/Dimension/Create Dimension/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Dimension/Create Dimension/request.json b/postman/cac/Dimension/Create Dimension/request.json index 5b3c18f69..69f459bbc 100644 --- a/postman/cac/Dimension/Create Dimension/request.json +++ b/postman/cac/Dimension/Create Dimension/request.json @@ -4,16 +4,25 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"dimension\": \"clientId\",\n \"priority\": 100,\n \"type\": \"STRING\"\n}", "options": { "raw": { "language": "json" } + }, + "raw_json_formatted": { + "dimension": "clientId", + "priority": 100, + "type": "STRING" } }, "url": { diff --git a/postman/cac/config/Get Config/.event.meta.json b/postman/cac/config/Get Config/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/config/Get Config/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/config/Get Config/request.json b/postman/cac/config/Get Config/request.json index 8eca2d4db..21f267f7e 100644 --- a/postman/cac/config/Get Config/request.json +++ b/postman/cac/config/Get Config/request.json @@ -4,7 +4,7 @@ { "key": "Authorization", "value": "Bearer {{token}}", - "type": "default" + "type": "text" } ], "url": { diff --git a/postman/experiment-platform-local-env.postman_environment.json b/postman/experiment-platform-local-env.postman_environment.json new file mode 100644 index 000000000..657343757 --- /dev/null +++ b/postman/experiment-platform-local-env.postman_environment.json @@ -0,0 +1,21 @@ +{ + "id": "70711214-9fb2-4d08-bd31-09c95e66dec7", + "name": "experiment-platform-local-env", + "values": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default", + "enabled": true + }, + { + "key": "token", + "value": "12345678", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-09T06:50:28.154Z", + "_postman_exported_using": "Postman/10.14.2" +} diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json new file mode 100644 index 000000000..68dbfb73d --- /dev/null +++ b/postman/experimentation-platform.postman_collection.json @@ -0,0 +1,807 @@ +{ + "info": { + "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "name": "experimentation-platform", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Create Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value1-control\",", + " \"pmTestKey2\": \"value1-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value2-test\",", + " \"pmTestKey2\": \"value2-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey1`,", + " `pmTestKey2`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, type: \"STRING\"},", + " {name: \"client\", priority: 100, type: \"STRING\"},", + " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"type\": dimension.type", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_default_config_keys();", + "create_dimensions();" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"experiment-1\",\n \"override_keys\": [\"pmTestKey1\", \"pmTestKey2\"],\n \"traffic_percentage\": 10,\n \"context\": {\n \"and\": [\n {\n \"==\": [\n {\n \"var\": \"os\"\n },\n \"ios\"\n ]\n },\n {\n \"==\": [\n {\n \"var\": \"client\"\n },\n \"testClientCac1\"\n ]\n }\n ]\n },\n \"variants\": [\n {\n \"id\": \"control\",\n \"variant_type\": \"CONTROL\",\n \"overrides\": {\n \"pmTestKey1\": \"value1-control\",\n \"pmTestKey2\": \"value1-control\"\n }\n },\n {\n \"id\": \"test1\",\n \"variant_type\": \"EXPERIMENTAL\",\n \"overrides\": {\n \"pmTestKey1\": \"value2-test\",\n \"pmTestKey2\": \"value2-test\"\n }\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Get Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Ramp", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + " console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`);", + " pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage);", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test traffic percentage\", function() {", + " const experiment_id = pm.environment.get(\"experiment_id\");", + "", + " fetch_experiment_n_test(experiment_id, 46);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"traffic_percentage\": 46\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/ramp", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "ramp" + ] + } + }, + "response": [] + }, + { + "name": "Conclude", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "const experiment_id = pm.environment.get(\"experiment_id\");", + "", + "function fetch_config_n_test(variants, winner_variant_id) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/config`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'Contet-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const config = response.json();", + " const contexts = config.contexts;", + " const overrides = config.overrides;", + "", + " const winner_variant = variants.find(variant => variant.id === winner_variant_id);", + " const winner_variant_override_id = winner_variant.override_id;", + " ", + " // there should be only one context with the winner variant override id", + " const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id));", + " console.log(\"Context with winner variant override\");", + " console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4));", + " pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1);", + "", + " // there should be 0 contexts with variant as a dimension", + " const contexts_with_variant_dim = contexts", + " .filter(", + " (context) => ", + " context.condition.and", + " ?.map(", + " (condition) => ", + " Object.keys(condition)", + " .map((k) => condition[k][0].var === \"variant\")", + " .reduce((p, c) => p || c, false))", + " .reduce((p, c) => p || c, false)", + " );", + " pm.expect(contexts_with_variant_dim.length).to.be.eq(0);", + "", + " // checking if winner override exists and is same as the expected override", + " const winner_variant_context = contexts_with_winner_variant_override[0]; ", + " pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1);", + " pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id));", + "", + " // checking if all the discarded overrides are removed", + " const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id);", + " const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id);", + " const available_overrides = Object.keys(overrides);", + " for(const ao of available_overrides) {", + " pm.expect(discarded_variants_override_ids).to.not.include(ao);", + " }", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const status = experiment.status;", + " pm.expect(status).to.be.eq(expected_status);", + "", + " const variants = experiment.variants;", + " fetch_config_n_test(variants, winner_variant_id);", + " });", + "}", + "", + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Conclude correctness\", function() {", + " const winner_variant_id = `${experiment_id}-control`;", + " fetch_experiment_n_test(experiment_id, winner_variant_id, \"CONCLUDED\")", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"winner_variant\": \"{{experiment_id}}-control\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/conclude", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "conclude" + ] + } + }, + "response": [] + }, + { + "name": "List experiments [No If-Modified-Since]", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "from_date", + "value": "2023-01-01%2000%3A00%3A00%20UTC" + }, + { + "key": "to_date", + "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Thu, 01 Jan 1970 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS,CONCLUDED" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"304 check\", function() {", + " pm.response.to.have.status(304);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Wed, 01 Jan 2070 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} \ No newline at end of file diff --git a/postman/experimentation-platform/.event.json b/postman/experimentation-platform/.event.json new file mode 100644 index 000000000..761eacf8f --- /dev/null +++ b/postman/experimentation-platform/.event.json @@ -0,0 +1,22 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} diff --git a/postman/experimentation-platform/.info.json b/postman/experimentation-platform/.info.json new file mode 100644 index 000000000..1d7a38f54 --- /dev/null +++ b/postman/experimentation-platform/.info.json @@ -0,0 +1,7 @@ +{ + "info": { + "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "name": "experimentation-platform", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/experimentation-platform/.meta.json b/postman/experimentation-platform/.meta.json new file mode 100644 index 000000000..f6742bd22 --- /dev/null +++ b/postman/experimentation-platform/.meta.json @@ -0,0 +1,11 @@ +{ + "childrenOrder": [ + "Create Experiment", + "Get Experiment", + "Ramp", + "Conclude", + "List experiments [No If-Modified-Since]", + "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", + "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]" + ] +} diff --git a/postman/experimentation-platform/.variable.json b/postman/experimentation-platform/.variable.json new file mode 100644 index 000000000..5460e39fb --- /dev/null +++ b/postman/experimentation-platform/.variable.json @@ -0,0 +1,14 @@ +{ + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/experimentation-platform/Conclude/.event.meta.json b/postman/experimentation-platform/Conclude/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Conclude/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Conclude/event.test.js b/postman/experimentation-platform/Conclude/event.test.js new file mode 100644 index 000000000..54793d344 --- /dev/null +++ b/postman/experimentation-platform/Conclude/event.test.js @@ -0,0 +1,97 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +const experiment_id = pm.environment.get("experiment_id"); + +function fetch_config_n_test(variants, winner_variant_id) { + const options = { + 'method': 'GET', + 'url': `${host}/config`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'Contet-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const config = response.json(); + const contexts = config.contexts; + const overrides = config.overrides; + + const winner_variant = variants.find(variant => variant.id === winner_variant_id); + const winner_variant_override_id = winner_variant.override_id; + + // there should be only one context with the winner variant override id + const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id)); + console.log("Context with winner variant override"); + console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4)); + pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1); + + // there should be 0 contexts with variant as a dimension + const contexts_with_variant_dim = contexts + .filter( + (context) => + context.condition.and + ?.map( + (condition) => + Object.keys(condition) + .map((k) => condition[k][0].var === "variant") + .reduce((p, c) => p || c, false)) + .reduce((p, c) => p || c, false) + ); + pm.expect(contexts_with_variant_dim.length).to.be.eq(0); + + // checking if winner override exists and is same as the expected override + const winner_variant_context = contexts_with_winner_variant_override[0]; + pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1); + pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id)); + + // checking if all the discarded overrides are removed + const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id); + const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id); + const available_overrides = Object.keys(overrides); + for(const ao of available_overrides) { + pm.expect(discarded_variants_override_ids).to.not.include(ao); + } + }); +} + +function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const status = experiment.status; + pm.expect(status).to.be.eq(expected_status); + + const variants = experiment.variants; + fetch_config_n_test(variants, winner_variant_id); + }); +} + +pm.test("200 OK", function() { + pm.response.to.have.status(200); +}); + +pm.test("Conclude correctness", function() { + const winner_variant_id = `${experiment_id}-control`; + fetch_experiment_n_test(experiment_id, winner_variant_id, "CONCLUDED") +}) \ No newline at end of file diff --git a/postman/experimentation-platform/Conclude/request.json b/postman/experimentation-platform/Conclude/request.json new file mode 100644 index 000000000..2d3272b8f --- /dev/null +++ b/postman/experimentation-platform/Conclude/request.json @@ -0,0 +1,37 @@ +{ + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "winner_variant": "{{experiment_id}}-control" + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/conclude", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "conclude" + ] + } +} diff --git a/postman/experimentation-platform/Conclude/response.json b/postman/experimentation-platform/Conclude/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Conclude/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Create Experiment/.event.meta.json b/postman/experimentation-platform/Create Experiment/.event.meta.json new file mode 100644 index 000000000..4ac527d83 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/experimentation-platform/Create Experiment/event.prerequest.js b/postman/experimentation-platform/Create Experiment/event.prerequest.js new file mode 100644 index 000000000..6ffe321ac --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/event.prerequest.js @@ -0,0 +1,77 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey1`, + `pmTestKey2` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +function create_dimensions() { + const dimensions = [ + {name: "os", priority: 10, type: "STRING"}, + {name: "client", priority: 100, type: "STRING"}, + {name: "variantIds", priority: 1000, type: "STRING"} + ]; + + for (const dimension of dimensions) { + const options = { + 'method': 'PUT', + 'url': `${host}/dimension`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "dimension": dimension.name, + "priority": dimension.priority, + "type": dimension.type + }) + } + }; + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating dimension: ${dimension.name}`); + console.log(error); + return; + } + console.log(`Created dimension: ${dimension.name}`); + }); + } +} + +create_default_config_keys(); +create_dimensions(); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/event.test.js b/postman/experimentation-platform/Create Experiment/event.test.js new file mode 100644 index 000000000..8fd9f2b6a --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/event.test.js @@ -0,0 +1,219 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const context = experiment.context; + console.log("Testing Context of Experiment"); + console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`); + console.log(`Actual: ${JSON.stringify(context, null, 2)}`); + pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context)); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test created contexts", function() { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + + const expected_context = { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + } + ] + }; + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey1": "value1-control", + "pmTestKey2": "value1-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey1": "value2-test", + "pmTestKey2": "value2-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/request.json b/postman/experimentation-platform/Create Experiment/request.json new file mode 100644 index 000000000..c101d0c77 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "name": "experiment-1", + "override_keys": [ + "pmTestKey1", + "pmTestKey2" + ], + "traffic_percentage": 10, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + } + ] + }, + "variants": [ + { + "id": "control", + "variant_type": "CONTROL", + "overrides": { + "pmTestKey1": "value1-control", + "pmTestKey2": "value1-control" + } + }, + { + "id": "test1", + "variant_type": "EXPERIMENTAL", + "overrides": { + "pmTestKey1": "value2-test", + "pmTestKey2": "value2-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } +} diff --git a/postman/experimentation-platform/Create Experiment/response.json b/postman/experimentation-platform/Create Experiment/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Get Experiment/.event.meta.json b/postman/experimentation-platform/Get Experiment/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Get Experiment/event.test.js b/postman/experimentation-platform/Get Experiment/event.test.js new file mode 100644 index 000000000..f0f823aa3 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/event.test.js @@ -0,0 +1,3 @@ +pm.test("200 OK", function() { + pm.response.to.have.status(200); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Get Experiment/request.json b/postman/experimentation-platform/Get Experiment/request.json new file mode 100644 index 000000000..0b12e225b --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/request.json @@ -0,0 +1,14 @@ +{ + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}" + ] + } +} diff --git a/postman/experimentation-platform/Get Experiment/response.json b/postman/experimentation-platform/Get Experiment/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js new file mode 100644 index 000000000..f57077057 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js @@ -0,0 +1,3 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json new file mode 100644 index 000000000..3031385c2 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json @@ -0,0 +1,37 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Thu, 01 Jan 1970 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS,CONCLUDED" + } + ] + } +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js new file mode 100644 index 000000000..e765cbe6c --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js @@ -0,0 +1,3 @@ +pm.test("304 check", function() { + pm.response.to.have.status(304); +}) \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json new file mode 100644 index 000000000..8823b1df7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json @@ -0,0 +1,37 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Wed, 01 Jan 2070 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json new file mode 100644 index 000000000..3a3ebbc86 --- /dev/null +++ b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json @@ -0,0 +1,40 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "from_date", + "value": "2023-01-01%2000%3A00%3A00%20UTC" + }, + { + "key": "to_date", + "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } +} diff --git a/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json b/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Ramp/.event.meta.json b/postman/experimentation-platform/Ramp/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Ramp/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Ramp/event.test.js b/postman/experimentation-platform/Ramp/event.test.js new file mode 100644 index 000000000..533ebfc9f --- /dev/null +++ b/postman/experimentation-platform/Ramp/event.test.js @@ -0,0 +1,37 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`); + pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage); + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test traffic percentage", function() { + const experiment_id = pm.environment.get("experiment_id"); + + fetch_experiment_n_test(experiment_id, 46); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Ramp/request.json b/postman/experimentation-platform/Ramp/request.json new file mode 100644 index 000000000..0809b22ed --- /dev/null +++ b/postman/experimentation-platform/Ramp/request.json @@ -0,0 +1,37 @@ +{ + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "traffic_percentage": 46 + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/ramp", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "ramp" + ] + } +} diff --git a/postman/experimentation-platform/Ramp/response.json b/postman/experimentation-platform/Ramp/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Ramp/response.json @@ -0,0 +1 @@ +[] From 744685309186407cb1bf15a44eba0eebd1297211 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 23 Aug 2023 16:33:14 +0530 Subject: [PATCH 094/352] fix: removed unwanted parameter to prevent warning --- crates/superposition_client/src/lib.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 43a6a0b04..addda0e84 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -9,8 +9,7 @@ use tokio::{ }; pub use types::{Config, Variants}; use types::{ - ExperimentStatusType, ExperimentStore, Experiments, ListExperimentsResponse, Variant, - VariantType, + ExperimentStore, Experiments, ListExperimentsResponse, Variant, VariantType, }; #[derive(Clone, Debug)] @@ -82,12 +81,9 @@ impl Client { let mut variants: Vec = Vec::new(); for exp in experiments { - if let Some(v) = self.decide_variant( - exp.traffic_percentage, - exp.variants, - toss, - exp.status, - ) { + if let Some(v) = + self.decide_variant(exp.traffic_percentage, exp.variants, toss) + { variants.push(v.id) } } @@ -106,7 +102,6 @@ impl Client { traffic: u8, applicable_variants: Variants, toss: i8, - status: ExperimentStatusType, ) -> Option { if toss < 0 { for variant in applicable_variants.iter() { From 509ee50e07cdee30325d57b734730734ba22816e Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Wed, 30 Aug 2023 17:33:46 +0530 Subject: [PATCH 095/352] test: update to latest newman that handles top level events and body lang type --- package-lock.json | 2 +- postman/cac/.event.json | 22 ------------------- postman/cac/.event.meta.json | 6 +++++ postman/cac/event.prerequest.js | 0 postman/cac/event.test.js | 0 postman/experimentation-platform/.event.json | 22 ------------------- .../experimentation-platform/.event.meta.json | 6 +++++ .../event.prerequest.js | 0 .../experimentation-platform/event.test.js | 0 9 files changed, 13 insertions(+), 45 deletions(-) delete mode 100644 postman/cac/.event.json create mode 100644 postman/cac/.event.meta.json create mode 100644 postman/cac/event.prerequest.js create mode 100644 postman/cac/event.test.js delete mode 100644 postman/experimentation-platform/.event.json create mode 100644 postman/experimentation-platform/.event.meta.json create mode 100644 postman/experimentation-platform/event.prerequest.js create mode 100644 postman/experimentation-platform/event.test.js diff --git a/package-lock.json b/package-lock.json index 5f3f53158..3204ced58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -783,7 +783,7 @@ }, "node_modules/newman": { "version": "5.3.2", - "resolved": "git+ssh://git@github.com/knutties/newman.git#4f7c11d69e61b3d1797ff2c56f2e87ddf375e442", + "resolved": "git+ssh://git@github.com/knutties/newman.git#37708c1fbf6b36240ac796a42dc5a7b435990764", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/postman/cac/.event.json b/postman/cac/.event.json deleted file mode 100644 index 761eacf8f..000000000 --- a/postman/cac/.event.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] -} diff --git a/postman/cac/.event.meta.json b/postman/cac/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/cac/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/cac/event.prerequest.js b/postman/cac/event.prerequest.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/cac/event.test.js b/postman/cac/event.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/experimentation-platform/.event.json b/postman/experimentation-platform/.event.json deleted file mode 100644 index 761eacf8f..000000000 --- a/postman/experimentation-platform/.event.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] -} diff --git a/postman/experimentation-platform/.event.meta.json b/postman/experimentation-platform/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/event.prerequest.js b/postman/experimentation-platform/event.prerequest.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/experimentation-platform/event.test.js b/postman/experimentation-platform/event.test.js new file mode 100644 index 000000000..e69de29bb From c86f6a783ab59fb072a02b035e82cc0c59d26cd2 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 16 Aug 2023 13:39:09 +0530 Subject: [PATCH 096/352] feat: Added Catch all error type for robust error handling --- .editorconfig | 4 +- Cargo.lock | 3 + .../src/api/errors.rs | 33 -- .../src/api/experiments/handlers.rs | 352 ++++++------------ .../src/api/experiments/helpers.rs | 190 ++++++---- .../experimentation-platform/src/api/mod.rs | 1 - crates/service-utils/Cargo.toml | 3 + crates/service-utils/src/errors/mod.rs | 1 + crates/service-utils/src/errors/types.rs | 150 ++++++++ crates/service-utils/src/helpers.rs | 1 - crates/service-utils/src/lib.rs | 2 + crates/service-utils/src/types.rs | 2 + 12 files changed, 402 insertions(+), 340 deletions(-) delete mode 100644 crates/experimentation-platform/src/api/errors.rs create mode 100644 crates/service-utils/src/errors/mod.rs create mode 100644 crates/service-utils/src/errors/types.rs create mode 100644 crates/service-utils/src/types.rs diff --git a/.editorconfig b/.editorconfig index 34811a401..8593e39a9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true charset = utf-8 indent_style = space indent_size = 4 -insert_final_newline = true +insert_final_newline = false trim_trailing_whitespace = true [*.md] @@ -16,4 +16,4 @@ trim_trailing_whitespace = false charset = utf-8 indent_style = tab -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/Cargo.lock b/Cargo.lock index 2a73286d2..2101b5ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,6 +2971,7 @@ dependencies = [ "actix-web", "base64 0.21.2", "bytes 1.4.0", + "derive_more", "diesel", "dotenv", "jsonschema", @@ -2979,6 +2980,8 @@ dependencies = [ "rusoto_core", "rusoto_kms", "rusoto_signature", + "serde", + "serde_json", "urlencoding", ] diff --git a/crates/experimentation-platform/src/api/errors.rs b/crates/experimentation-platform/src/api/errors.rs deleted file mode 100644 index 1b8688bc9..000000000 --- a/crates/experimentation-platform/src/api/errors.rs +++ /dev/null @@ -1,33 +0,0 @@ -use actix_web::{ - error, - http::{header::ContentType, StatusCode}, - web::Json, - HttpResponse, -}; -use derive_more::{Display, Error}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Error, Display)] -#[display(fmt = "experiment error: {}", message)] -pub struct AppError { - pub message: String, - pub possible_fix: String, - pub status_code: StatusCode, -} - -#[derive(Deserialize, Serialize)] -struct ErrorResponse { - message: String, - possible_fix: String, -} - -impl error::ResponseError for AppError { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code) - .insert_header(ContentType::json()) - .json(Json(ErrorResponse { - message: self.message.to_string(), - possible_fix: self.possible_fix.to_string(), - })) - } -} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 953b15bbd..ee3db8699 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -1,16 +1,18 @@ use std::collections::HashSet; use actix_web::{ - get, - http::StatusCode, - patch, post, + get, patch, post, web::{self, Data, Json, Query}, HttpRequest, HttpResponse, Scope, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use service_utils::service::types::{AppState, AuthenticationInfo, DbConnection}; +use service_utils::{ + errors::types::{Error as err, ErrorResponse}, + service::types::{AppState, AuthenticationInfo, DbConnection}, + types as app, +}; use super::{ helpers::{ @@ -20,11 +22,11 @@ use super::{ types::{ ConcludeExperimentRequest, ContextAction, ContextPutReq, ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, - ExperimentsResponse, RampRequest, Variant, + ExperimentsResponse, ListFilters, RampRequest, Variant, }, }; + use crate::{ - api::{errors::AppError, experiments::types::ListFilters}, db::models::{Experiment, ExperimentStatusType}, db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, }; @@ -44,58 +46,53 @@ async fn create( req: web::Json, auth_info: AuthenticationInfo, db_conn: DbConnection, -) -> actix_web::Result> { +) -> app::Result> { use crate::db::schema::cac_v1::experiments::dsl::experiments; let DbConnection(mut conn) = db_conn; let override_keys = &req.override_keys; let mut variants = req.variants.to_vec(); - let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter( - variants - .iter() - .map(|v| v.id.as_str()) - ); + let unique_ids_of_variants_from_req: HashSet<&str> = + HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); if unique_ids_of_variants_from_req.len() != variants.len() { - return Err(actix_web::error::ErrorBadRequest( - "variant ids are expected to be unique".to_string(), - )); + return Err(err::BadRequest(ErrorResponse { + message: "variant ids are expected to be unique".to_string(), + possible_fix: "provide unqiue variant IDs".to_string(), + })); } // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant - check_variant_types(&variants) - .map_err(|e| actix_web::error::ErrorBadRequest(e.to_string()))?; + check_variant_types(&variants)?; // Checking if all the variants are overriding the mentioned keys let are_valid_variants = check_variants_override_coverage(&variants, override_keys); if !are_valid_variants { - return Err(actix_web::error::ErrorBadRequest( - "all variants should contain the keys mentioned override_keys".to_string(), - )); + return Err(err::BadRequest(ErrorResponse { + message: "all variants should contain the keys mentioned in override_keys" + .to_string(), + possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", override_keys.join(",")) + })); } // Checking if context is a key-value pair map if !req.context.is_object() { - return Err(actix_web::error::ErrorBadRequest( - "context should be map of key value pairs".to_string(), - )); + return Err(err::BadRequest(ErrorResponse { + message: "context should be map of key value pairs".to_string(), + possible_fix: "Please refer documentation or contact an admin".to_string(), + })); } // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; - match validate_experiment(&req, &flags, &mut conn) { - Ok(valid) => { - if !valid { - return Err(actix_web::error::ErrorBadRequest( - "invalid experiment config".to_string(), - )); - } - } - Err(_) => { - return Err(actix_web::error::ErrorInternalServerError("")); - } + let (valid, reason) = validate_experiment(&req, &flags, &mut conn)?; + if !valid { + return Err(err::BadRequest(ErrorResponse { + message: reason, + possible_fix: "Please refer documentation or contact an admin".to_string(), + })); } // generating snowflake id for experiment @@ -111,13 +108,14 @@ async fn create( variant.id = variant_id.to_string(); let updated_cacccontext = - add_variant_dimension_to_ctx(&req.context, variant_id.to_string()) - .map_err(|_| actix_web::error::ErrorInternalServerError(""))?; + add_variant_dimension_to_ctx(&req.context, variant_id.to_string())?; let payload = ContextPutReq { context: updated_cacccontext .as_object() - .ok_or(actix_web::error::ErrorInternalServerError(""))? + .ok_or(err::InternalServerErr( + "Could not convert updated CAC context to serde Object".to_string(), + ))? .clone(), r#override: variant.overrides.clone(), }; @@ -133,15 +131,9 @@ async fn create( .bearer_auth(&state.admin_token) .json(&cac_operations) .send() - .map_err(|e| { - log::info!("failed to create contexts in cac: {e}"); - actix_web::error::ErrorInternalServerError("") - })? + .map_err(|e| err::InternalServerErr(e.to_string()))? .json::>() - .map_err(|e| { - log::info!("failed to parse response: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + .map_err(|e| err::InternalServerErr(e.to_string()))?; // updating variants with context and override ids for i in 0..created_contexts.len() { @@ -167,24 +159,14 @@ async fn create( last_modified_by: email, }; - let insert = diesel::insert_into(experiments) + let mut inserted_experiments = diesel::insert_into(experiments) .values(&new_experiment) - .get_results(&mut conn); + .get_results(&mut conn)?; - match insert { - Ok(mut inserted_experiments) => { - let inserted_experiment: Experiment = inserted_experiments.remove(0); - let response = ExperimentCreateResponse::from(inserted_experiment); + let inserted_experiment: Experiment = inserted_experiments.remove(0); + let response = ExperimentCreateResponse::from(inserted_experiment); - return Ok(Json(response)); - } - Err(e) => { - log::info!("Experiment creation failed with error: {e}"); - return Err(actix_web::error::ErrorInternalServerError( - "Failed to create experiment".to_string(), - )); - } - } + return Ok(Json(response)); } #[patch("/{experiment_id}/conclude")] @@ -194,53 +176,42 @@ async fn conclude( req: web::Json, db_conn: DbConnection, auth_info: AuthenticationInfo, -) -> actix_web::Result> { +) -> app::Result> { use crate::db::schema::cac_v1::experiments::dsl; let experiment_id: i64 = path.into_inner(); let winner_variant_id: String = req.into_inner().winner_variant.to_owned(); let DbConnection(mut conn) = db_conn; - let db_result = dsl::experiments + let experiment: Experiment = dsl::experiments .find(experiment_id) - .get_result::(&mut conn); - - let experiment = match db_result { - Ok(response) => response, - Err(diesel::result::Error::NotFound) => { - return Err(actix_web::error::ErrorNotFound("experiment not found")); - } - Err(e) => { - log::info!("failed to fetch experiment from db: {e}"); - return Err(actix_web::error::ErrorInternalServerError( - "something went wrong.", - )); - } - }; + .get_result::(&mut conn)?; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { - return Err(actix_web::error::ErrorBadRequest( - "experiment is already concluded", - )); + return Err(err::BadRequest(ErrorResponse { + message: format!("experiment with id {} is already concluded", experiment_id), + possible_fix: "Try to conclude a different experiment".to_string(), + })); } - let experiment_context = experiment - .context - .as_object() - .ok_or(actix_web::error::ErrorInternalServerError(""))?; + let experiment_context = + experiment + .context + .as_object() + .ok_or(err::InternalServerErr( + "Could not convert the context read from DB to JSON object".to_string(), + ))?; let mut operations: Vec = vec![]; - let experiment_variants: Vec = serde_json::from_value(experiment.variants) - .map_err(|e| { - log::error!("parsing to variant type failed with err: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + let experiment_variants: Vec = + serde_json::from_value(experiment.variants) + .map_err(|e| err::InternalServerErr(e.to_string()))?; let mut is_valid_winner_variant = false; for variant in experiment_variants { - let context_id = variant - .context_id - .ok_or(actix_web::error::ErrorInternalServerError(""))?; + let context_id = variant.context_id.ok_or(err::InternalServerErr( + "Could not read context ID from experiment".to_string(), + ))?; if variant.id == winner_variant_id { let context_put_req = ContextPutReq { @@ -257,7 +228,12 @@ async fn conclude( } if !is_valid_winner_variant { - return Err(actix_web::error::ErrorNotFound("winner varaint not found")); + return Err(err::NotFound(ErrorResponse { + message: "winner variant not found".to_string(), + possible_fix: + "A wrong variant ID may have been sent, please check and try again" + .to_string(), + })); } // calling CAC bulk api with operations as payload @@ -268,36 +244,28 @@ async fn conclude( .bearer_auth(&state.admin_token) .json(&operations) .send() - .map_err(|e| { - log::info!("Failed to update contexts in CAC: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + .map_err(|e| err::InternalServerErr(e.to_string()))?; if !response.status().is_success() { - return Err(actix_web::error::ErrorInternalServerError("")); + return Err(err::InternalServerErr(format!( + "Request to {} failed with response: {:?}", + url, response + ))); } let AuthenticationInfo(email) = auth_info; // updating experiment status in db - let experiment_update_result = diesel::update(dsl::experiments) + let updated_experiment = diesel::update(dsl::experiments) .filter(dsl::id.eq(experiment_id)) .set(( dsl::status.eq(ExperimentStatusType::CONCLUDED), dsl::last_modified.eq(Utc::now()), dsl::last_modified_by.eq(email), )) - .get_result::(&mut conn); + .get_result::(&mut conn)?; - match experiment_update_result { - Ok(updated_experiment) => { - return Ok(Json(ExperimentResponse::from(updated_experiment))); - } - Err(e) => { - log::info!("Failed to updated experiment status: {e}"); - return Err(actix_web::error::ErrorInternalServerError("")); - } - } + return Ok(Json(ExperimentResponse::from(updated_experiment))); } #[get("")] @@ -305,21 +273,13 @@ async fn list_experiments( req: HttpRequest, filters: Query, db_conn: DbConnection, -) -> actix_web::Result { +) -> app::Result { let DbConnection(mut conn) = db_conn; let max_event_timestamp: Option = event_log::event_log .filter(event_log::table_name.eq("experiments")) .select(diesel::dsl::max(event_log::timestamp)) - .first(&mut conn) - .map_err(|e| { - println!("error fetching max_event_timestamp: {e}"); - AppError { - message: String::from("Something went wrong"), - possible_fix: String::from("Please try again later"), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - } - })?; + .first(&mut conn)?; let last_modified = req .headers() @@ -343,8 +303,7 @@ async fn list_experiments( let now = Utc::now(); builder .filter( - experiments::last_modified - .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + experiments::last_modified.ge(filters.from_date.unwrap_or(now - Duration::hours(24))), ) .filter(experiments::last_modified.le(filters.to_date.unwrap_or(now))) }; @@ -359,81 +318,37 @@ async fn list_experiments( .limit(limit) .offset(offset); - let number_of_experiments = match count_query.count().get_result(&mut conn) { - Ok(count) => count, - Err(diesel::result::Error::NotFound) => 0, - Err(e) => { - println!( - "Error occurred while counting items in list experiments API {:?}", - e - ); - return Err(AppError { - message: String::from("Something went wrong"), - possible_fix: String::from("Please try again later"), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }); - } - }; + let number_of_experiments = count_query.count().get_result(&mut conn)?; - let db_result = query.load::(&mut conn); + let experiment_list = query.load::(&mut conn)?; let total_pages = (number_of_experiments as f64 / limit as f64).ceil() as i64; - match db_result { - Ok(response) => { - return Ok(HttpResponse::Ok().json(ExperimentsResponse { - total_items: number_of_experiments, - total_pages: total_pages, - data: response - .into_iter() - .map(|entry| ExperimentResponse::from(entry)) - .collect(), - })) - } - Err(diesel::result::Error::NotFound) => Err(AppError { - message: String::from("No results found for your query"), - possible_fix: String::from("Update your filter parameters"), - status_code: StatusCode::NOT_FOUND, - }), - Err(e) => { - println!("Error occurred in list experiments API {:?}", e); - Err(AppError { - message: String::from("Something went wrong"), - possible_fix: String::from("Please try again later"), - status_code: StatusCode::INTERNAL_SERVER_ERROR, - }) - } - } + Ok(HttpResponse::Ok().json(ExperimentsResponse { + total_items: number_of_experiments, + total_pages: total_pages, + data: experiment_list + .into_iter() + .map(|entry| ExperimentResponse::from(entry)) + .collect(), + })) } #[get("/{id}")] async fn get_experiment( params: web::Path, db_conn: DbConnection, -) -> actix_web::Result> { +) -> app::Result> { use crate::db::schema::cac_v1::experiments::dsl::*; let experiment_id = params.into_inner(); let DbConnection(mut conn) = db_conn; - let db_result = experiments + let result: Experiment = experiments .find(experiment_id) - .get_result::(&mut conn); - - let response = match db_result { - Ok(result) => ExperimentResponse::from(result), - Err(diesel::result::Error::NotFound) => { - return Err(actix_web::error::ErrorNotFound( - "Experiment not found".to_string(), - )); - } - Err(e) => { - log::error!("{}", format!("get experiments failed due to : {e:?}")); - return Err(actix_web::error::ErrorInternalServerError("")); - } - }; + .get_result::(&mut conn)?; - return Ok(Json(response)); + return Ok(Json(ExperimentResponse::from(result))); } #[patch("/{id}/ramp")] @@ -442,55 +357,42 @@ async fn ramp( req: web::Json, db_conn: DbConnection, auth_info: AuthenticationInfo, -) -> actix_web::Result> { +) -> app::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); use crate::db::schema::cac_v1::experiments::dsl::*; - let db_result: Result = - experiments.find(exp_id).get_result::(&mut conn); - - let experiment = match db_result { - Ok(result) => result, - Err(diesel::result::Error::NotFound) => { - return Err(actix_web::error::ErrorNotFound("No results found")) - } - Err(e) => { - log::info!("{e}"); - return Err(actix_web::error::ErrorInternalServerError( - "Something went wrong", - )); - } - }; + let experiment: Experiment = experiments + .find(exp_id) + .get_result::(&mut conn)?; let old_traffic_percentage = experiment.traffic_percentage as u8; let new_traffic_percentage = req.traffic_percentage as u8; - let experiment_variants: Vec = serde_json::from_value(experiment.variants) - .map_err(|e| { - log::error!("parsing to variant type failed with err: {e}"); - actix_web::error::ErrorInternalServerError("") - })?; + let experiment_variants: Vec = + serde_json::from_value(experiment.variants) + .map_err(|e| err::InternalServerErr(e.to_string()))?; let variants_count = experiment_variants.len() as u8; let max = 100 / variants_count; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { - return Err(actix_web::error::ErrorBadRequest( - "Experiment is already concluded", - )); + return Err(err::BadRequest(ErrorResponse { + message: "Experiment is already concluded".to_string(), + possible_fix: "".to_string(), + })); } else if new_traffic_percentage > max { - log::info!("The Traffic percentage provided exceeds the range"); - return Err(actix_web::error::ErrorBadRequest(format!( - "The traffic_percentage cannot exceed {}", - max - ))); + return Err(err::BadRequest(ErrorResponse { + message: format!("The traffic_percentage cannot exceed {}", max), + possible_fix: format!("Provide a traffic percentage less than {}", max), + })); } else if new_traffic_percentage == old_traffic_percentage { - return Err(actix_web::error::ErrorBadRequest( - "The traffic_percentage is same as provided", - )); + return Err(err::BadRequest(ErrorResponse { + message: "The traffic_percentage is same as provided".to_string(), + possible_fix: "".to_string(), + })); } let AuthenticationInfo(email) = auth_info; - let update = diesel::update(experiments) + let new_traffic_percentage = diesel::update(experiments) .filter(id.eq(exp_id)) .set(( traffic_percentage.eq(req.traffic_percentage as i32), @@ -498,25 +400,15 @@ async fn ramp( last_modified_by.eq(email), status.eq(ExperimentStatusType::INPROGRESS), )) - .execute(&mut conn); + .execute(&mut conn)?; - match update { - Ok(0) => { - return Err(actix_web::error::ErrorInternalServerError( - "Failed to update the traffic_percentage", - )) - } - Ok(_) => { - return Ok(Json(format!( - "Traffic percentage has been updated for the experiment id : {}", - exp_id - ))) - } - Err(e) => { - log::info!("Failed to update the traffic_percentage: {e}"); - return Err(actix_web::error::ErrorInternalServerError( - "Failed to update the traffic_percentage", - )); - } + if new_traffic_percentage == 0 { + return Err(err::InternalServerErr( + "Failed to update the traffic_percentage".to_string(), + )); } + return Ok(Json(format!( + "Traffic percentage has been updated for the experiment id : {}", + exp_id + ))); } diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index c01d49a5a..a5fbc7778 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -3,10 +3,12 @@ use crate::db::models::{Experiment, ExperimentStatusType}; use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value}; +use service_utils::errors::types::ErrorResponse; use service_utils::service::types::ExperimentationFlags; +use service_utils::{errors::types::Error as err, types as app}; use std::collections::HashSet; -pub fn check_variant_types(variants: &Vec) -> Result<(), &'static str> { +pub fn check_variant_types(variants: &Vec) -> app::Result<()> { let mut experimental_variant_cnt = 0; let mut control_variant_cnt = 0; @@ -22,59 +24,94 @@ pub fn check_variant_types(variants: &Vec) -> Result<(), &'static str> } if control_variant_cnt > 1 || control_variant_cnt == 0 { - return Err("experiment should have exactly 1 control variant."); + return Err(err::BadArgument(ErrorResponse { + message: "experiment should have exactly 1 control variant".to_string(), + possible_fix: "ensure only one control variant is present".to_string(), + })); } else if experimental_variant_cnt < 1 { - return Err("experiment should have atlease 1 experimental variant"); + return Err(err::BadArgument(ErrorResponse { + message: "experiment should have at least 1 experimental variant".to_string(), + possible_fix: "ensure only one control variant is present".to_string(), + })); } Ok(()) } -pub fn extract_dimensions( - context_json: &Value, -) -> Result, &'static str> { +pub fn extract_dimensions(context_json: &Value) -> app::Result> { // Assuming max 2-level nesting in context json logic let context = context_json .as_object() - .ok_or("extract_dimensions: context not an object")?; + .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: context not a valid JSON object".to_string(), possible_fix: "send a valid JSON context".to_string() }))?; let conditions = match context.get("and") { Some(conditions_json) => conditions_json .as_array() - .ok_or("extract_dimension: failed parsing conditions as an array")? + .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: failed parsing conditions as an array".to_string(), possible_fix: "ensure the context provided obeys the rules of JSON logic".to_string() }))? .clone(), None => vec![context_json.clone()], }; let mut dimension_tuples = Vec::new(); for condition in &conditions { - let condition_obj = condition - .as_object() - .ok_or("extract_dimensions: failed to parse condition as an object")?; + let condition_obj = + condition.as_object().ok_or(err::BadArgument(ErrorResponse { + message: " failed to parse condition as an object".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; let operators = condition_obj.keys(); for operator in operators { - let operands = condition_obj[operator] - .as_array() - .ok_or("extract_dimension: failed to parse operands as an arrays")?; + let operands = condition_obj[operator].as_array().ok_or(err::BadArgument( + ErrorResponse { + message: " failed to parse operands as an arrays".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }, + ))?; let variable_name = operands .get(0) // getting first element which should contain an object with property `var` with string value - .ok_or( - "extract_dimension: failed to get variable name from operands list", - )? + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable name from operands list" + .to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))? .as_object() // parsing json value as an object/map - .ok_or("extract_dimension: failed to parse variable as an object")? + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to parse variable as an object".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))? .get("var") // accessing `var` from object/map which contains variable name - .ok_or("extract_dimension: var property not present in variable object")? + .ok_or(err::BadArgument(ErrorResponse { + message: " var property not present in variable object".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))? .as_str() // parsing json value as raw string - .ok_or("extract_dimension: var propery value is not a string")?; + .ok_or(err::BadArgument(ErrorResponse { + message: " var propery value is not a string".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; let variable_value = operands .get(1) // getting second element which should be the value of the variable - .ok_or( - "extract_dimension: failed to get variable value from operands list", - )?; + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable value from operands list" + .to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; dimension_tuples.push((String::from(variable_name), variable_value.clone())); } @@ -83,10 +120,7 @@ pub fn extract_dimensions( Ok(Map::from_iter(dimension_tuples)) } -pub fn are_overlapping_contexts( - context_a: &Value, - context_b: &Value, -) -> Result { +pub fn are_overlapping_contexts(context_a: &Value, context_b: &Value) -> app::Result { let dimensions_a = extract_dimensions(context_a)?; let dimensions_b = extract_dimensions(context_b)?; @@ -144,7 +178,7 @@ pub fn validate_experiment( experiment: &ExperimentCreateRequest, flags: &ExperimentationFlags, conn: &mut PgConnection, -) -> Result { +) -> app::Result<(bool, String)> { use crate::db::schema::cac_v1::experiments::dsl::*; let created_perdicate = status.eq(ExperimentStatusType::CREATED); @@ -152,65 +186,75 @@ pub fn validate_experiment( let active_experiments_filter = experiments.filter(created_perdicate.or(inprogress_predicate)); - let active_experiments: Vec = - active_experiments_filter.load(conn).map_err(|e| { - log::info!("validate_experiment: {e}"); - "Failed to fetch active experiments" - })?; + let active_experiments: Vec = active_experiments_filter.load(conn)?; let mut valid_experiment = true; - if !flags.allow_same_keys_overlapping_ctx - || !flags.allow_diff_keys_overlapping_ctx - || !flags.allow_same_keys_non_overlapping_ctx + let mut invalid_reason = String::new(); + if flags.allow_same_keys_overlapping_ctx + && flags.allow_diff_keys_overlapping_ctx + && flags.allow_same_keys_non_overlapping_ctx { - let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); - for active_experiment in active_experiments.iter() { - let are_overlapping = - are_overlapping_contexts(&experiment.context, &active_experiment.context) - .map_err(|e| { - log::info!("validate_experiment: {e}"); - "Failed to validate for overlapping context" - })?; - - let have_intersecting_key_set = active_experiment - .override_keys - .iter() - .any(|key| override_keys_set.contains(key)); - - if !flags.allow_diff_keys_overlapping_ctx { - valid_experiment = - valid_experiment && !(are_overlapping && !have_intersecting_key_set); - } - if !flags.allow_same_keys_overlapping_ctx { - valid_experiment = - valid_experiment && !(are_overlapping && have_intersecting_key_set); - } - if !flags.allow_same_keys_non_overlapping_ctx { - valid_experiment = - valid_experiment && !(!are_overlapping && have_intersecting_key_set); - } + return Ok((valid_experiment, invalid_reason)); + } + let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); + for active_experiment in active_experiments.iter() { + let are_overlapping = + are_overlapping_contexts(&experiment.context, &active_experiment.context) + .map_err(|e| { + log::info!("validate_experiment: {e}"); + err::BadArgument(ErrorResponse { + message: "Failed to validate for overlapping context. One of the current running experiments already has this context or overlaps with it".into(), + possible_fix: "Overlapping contexts are not allowed currently as per your configuration of CAC".into(), + }) + })?; + + let have_intersecting_key_set = active_experiment + .override_keys + .iter() + .any(|key| override_keys_set.contains(key)); + + if !flags.allow_diff_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && !have_intersecting_key_set); + } + if !flags.allow_same_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && have_intersecting_key_set); + } + if !flags.allow_same_keys_non_overlapping_ctx { + valid_experiment = + valid_experiment && !(!are_overlapping && have_intersecting_key_set); + } - if !valid_experiment { - break; - } + if !valid_experiment { + invalid_reason.push_str("This current context overlaps with an existing experiment or the keys in the context are overlapping"); + break; } } - Ok(valid_experiment) + Ok((valid_experiment, invalid_reason)) } pub fn add_variant_dimension_to_ctx( context_json: &Value, variant: String, -) -> Result { +) -> app::Result { let context = context_json .as_object() - .ok_or("extract_dimensions: context not an object")?; + .ok_or(err::BadArgument(ErrorResponse { + message: "context not an object".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; let mut conditions = match context.get("and") { Some(conditions_json) => conditions_json .as_array() - .ok_or("extract_dimension: failed parsing conditions as an array")? + .ok_or(err::BadArgument(ErrorResponse { + message: " failed parsing conditions as an array".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))? .clone(), None => vec![context_json.clone()], }; @@ -228,9 +272,9 @@ pub fn add_variant_dimension_to_ctx( match serde_json::to_value(updated_ctx) { Ok(value) => Ok(value), - Err(e) => { - log::info!("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value {e}"); - Err("add_variant_dimension_to_ctx: Failed to convert context to serde_json::Value") - } + Err(_) => Err(err::BadArgument(ErrorResponse { + message: "Failed to convert context to a valid JSON object".to_string(), + possible_fix: "Check the request sent for correctness".to_string(), + })), } } diff --git a/crates/experimentation-platform/src/api/mod.rs b/crates/experimentation-platform/src/api/mod.rs index f6e673e65..c5b8fbb5b 100644 --- a/crates/experimentation-platform/src/api/mod.rs +++ b/crates/experimentation-platform/src/api/mod.rs @@ -1,2 +1 @@ -pub mod errors; pub mod experiments; diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index edc954b02..9976c4cbb 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -23,3 +23,6 @@ base64 = "0.21.2" urlencoding = "~2.1.2" jsonschema = "~0.17" log = "^0.4" +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +derive_more = "^0.99" diff --git a/crates/service-utils/src/errors/mod.rs b/crates/service-utils/src/errors/mod.rs new file mode 100644 index 000000000..dd198c6d0 --- /dev/null +++ b/crates/service-utils/src/errors/mod.rs @@ -0,0 +1 @@ +pub mod types; \ No newline at end of file diff --git a/crates/service-utils/src/errors/types.rs b/crates/service-utils/src/errors/types.rs new file mode 100644 index 000000000..b1a8fb4d3 --- /dev/null +++ b/crates/service-utils/src/errors/types.rs @@ -0,0 +1,150 @@ +use actix_web::{ + error, + http::{header::ContentType, StatusCode}, + web::Json, + HttpResponse, +}; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use std::error as err; + +#[derive(Debug, Error, Display, Clone)] +#[display( + fmt = "server returned an error: {} with status code {}", + message, + status_code +)] +pub struct ServerError { + pub message: String, + pub possible_fix: String, + pub status_code: StatusCode, +} + +impl error::ResponseError for ServerError { + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code) + .insert_header(ContentType::json()) + .json(Json(ErrorResponse { + message: self.message.to_string(), + possible_fix: self.possible_fix.to_string(), + })) + } +} + +#[derive(Deserialize, Serialize, Debug, Display, Error, Clone)] +#[display( + fmt = "error sent to client {} that can be fixed by {}", + message, + possible_fix +)] +pub struct ErrorResponse { + pub message: String, + pub possible_fix: String, +} + +#[derive(Debug, Display, Clone)] +pub enum Error { + #[display(fmt = "Connection failed to {}, reason: {}", "_0", "_1")] + ConnectionFailed(String, String), + #[display(fmt = "Error occured with postgres: {}", "_0")] + DB(String), + #[display(fmt = "The resource(s) requested were not found: {}", "_0")] + NotFound(ErrorResponse), + #[display(fmt = "Bad Request: {}", "_0")] + BadRequest(ErrorResponse), + #[display(fmt = "Bad Arugment: {}", "_0")] + BadArgument(ErrorResponse), + #[display(fmt = "Something went wrong, reason: {}", "_0")] + InternalServerErr(String), + #[display(fmt = "{}", "_0")] + Generic(ServerError), +} + +impl err::Error for Error {} + +impl From for Error { + fn from(value: diesel::result::Error) -> Self { + match value { + diesel::result::Error::InvalidCString(e) => { + Self::ConnectionFailed("DATABASE".into(), e.to_string()) + } + diesel::result::Error::DatabaseError(kind, error) => Self::DB(format!( + "Database error of kind {:?} occurred due to {}", + kind, + error.message() + )), + diesel::result::Error::NotFound => Self::NotFound(ErrorResponse { + message: "No records found".into(), + possible_fix: "Please refine or correct your search parameters".into(), + }), + diesel::result::Error::DeserializationError(_) => { + Self::BadRequest(ErrorResponse { + message: "The server could not understand your request".into(), + possible_fix: "Please check your request for issues".into(), + }) + } + diesel::result::Error::SerializationError(_) => { + Self::BadRequest(ErrorResponse { + message: "The server could not understand your request".into(), + possible_fix: "Please check your request for issues".into(), + }) + } + e => Self::InternalServerErr(format!("What went wrong: {:?}", e)), + } + } +} + +impl Error { + fn generate_err_response( + code: StatusCode, + message: String, + possible_fix: String, + ) -> HttpResponse { + HttpResponse::build(code) + .insert_header(ContentType::json()) + .json(Json(ErrorResponse { + message, + possible_fix, + })) + } +} + +impl error::ResponseError for Error { + fn error_response(&self) -> HttpResponse { + let please_try_again: String = String::from("Please try again later"); + log::error!("{}", self); + match self { + Error::ConnectionFailed(_, _) => Self::generate_err_response( + StatusCode::SERVICE_UNAVAILABLE, + "Something went wrong".into(), + please_try_again, + ), + Error::DB(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong".into(), + please_try_again, + ), + Error::NotFound(reason) => Self::generate_err_response( + StatusCode::NOT_FOUND, + reason.message.clone(), + reason.possible_fix.clone(), + ), + Error::BadRequest(cause) => Self::generate_err_response( + StatusCode::BAD_REQUEST, + cause.message.clone(), + cause.possible_fix.clone(), + ), + Error::BadArgument(cause) => Self::generate_err_response( + StatusCode::BAD_REQUEST, + cause.message.clone(), + cause.possible_fix.clone(), + ), + Error::InternalServerErr(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong".into(), + please_try_again, + ), + Error::Generic(server_error) => server_error.error_response(), + } + } +} diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 6b3ed7f57..6ba9dc560 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -46,5 +46,4 @@ where } } - // pub fn db_connection_from_state(state: T) -> diff --git a/crates/service-utils/src/lib.rs b/crates/service-utils/src/lib.rs index e846a2af8..f32de15cb 100644 --- a/crates/service-utils/src/lib.rs +++ b/crates/service-utils/src/lib.rs @@ -2,3 +2,5 @@ pub mod aws; pub mod db; pub mod helpers; pub mod service; +pub mod errors; +pub mod types; \ No newline at end of file diff --git a/crates/service-utils/src/types.rs b/crates/service-utils/src/types.rs new file mode 100644 index 000000000..7ad0bab50 --- /dev/null +++ b/crates/service-utils/src/types.rs @@ -0,0 +1,2 @@ + +pub type Result = core::result::Result; \ No newline at end of file From f5a0d590d7e58d735ab4f784c5b7105433a31183 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 16 Aug 2023 13:39:09 +0530 Subject: [PATCH 097/352] feat: Added Catch all error type for robust error handling --- .../src/api/experiments/helpers.rs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index a5fbc7778..d45d90da1 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -55,11 +55,14 @@ pub fn extract_dimensions(context_json: &Value) -> app::Result app::Result app::Result { +pub fn are_overlapping_contexts( + context_a: &Value, + context_b: &Value, +) -> app::Result { let dimensions_a = extract_dimensions(context_a)?; let dimensions_b = extract_dimensions(context_b)?; @@ -190,15 +196,13 @@ pub fn validate_experiment( let mut valid_experiment = true; let mut invalid_reason = String::new(); - if flags.allow_same_keys_overlapping_ctx - && flags.allow_diff_keys_overlapping_ctx - && flags.allow_same_keys_non_overlapping_ctx + if !flags.allow_same_keys_overlapping_ctx + || !flags.allow_diff_keys_overlapping_ctx + || !flags.allow_same_keys_non_overlapping_ctx { - return Ok((valid_experiment, invalid_reason)); - } - let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); - for active_experiment in active_experiments.iter() { - let are_overlapping = + let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); + for active_experiment in active_experiments.iter() { + let are_overlapping = are_overlapping_contexts(&experiment.context, &active_experiment.context) .map_err(|e| { log::info!("validate_experiment: {e}"); @@ -226,9 +230,10 @@ pub fn validate_experiment( valid_experiment && !(!are_overlapping && have_intersecting_key_set); } - if !valid_experiment { - invalid_reason.push_str("This current context overlaps with an existing experiment or the keys in the context are overlapping"); - break; + if !valid_experiment { + invalid_reason.push_str("This current context overlaps with an existing experiment or the keys in the context are overlapping"); + break; + } } } From caa805fadae4ec8b9b06de806cfa252ad83a13cc Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Tue, 22 Aug 2023 19:56:08 +0530 Subject: [PATCH 098/352] ci: added cocogitto config for automatic versioning --- Jenkinsfile | 115 ++++++++++++++++++++++++++++++++++++++++++++++++---- cog.toml | 33 +++++++++++++++ 2 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 cog.toml diff --git a/Jenkinsfile b/Jenkinsfile index e85f310fb..94241b365 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,36 +5,132 @@ def getRegistryHost(aws_acc_id, region) { pipeline { agent { label 'hypersdk' } environment { - SKIP_CI = false; REGION = "ap-south-1"; REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" + GIT_REPO_NAME = "context-aware-config" + SSH_AUTH_SOCK = """${sh( + returnStdout: true, + script: ''' + eval $(ssh-agent) > /dev/null + ssh-add /home/jenkins/.ssh/id_rsa + echo $SSH_AUTH_SOCK + ''' + )}""" } stages { stage('Checkout') { steps { script { - isSkipCI = sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", - returnStatus: true) + isSkipCI = sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", returnStatus: true) if (isSkipCI == 0) { - env.SKIP_CI = true; + env.SKIP_CI = true; + } else { + env.SKIP_CI = false; } env.COMMIT_HASH = sh(returnStdout: true, script: "git rev-parse --short HEAD").trim() } } } + stage('Get Git Creds') { + steps { + script { + sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' + sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' + sh 'git fetch' + sh 'git config user.name ""Jenkins User""' + sh 'git config user.email bitbucket.jenkins.read@juspay.in' + } + } + } + stage('Test') { when { expression { SKIP_CI == 'false' } } steps { sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' } } + stage('Cargo Install') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh 'cargo install cocogitto@5.5.0 cargo-edit' + } + } + + stage('Get old Version') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + env.BUILD_PIPELINE_RUNNING=true + env.COMMIT_MSG="""${sh(returnStdout: true, script: "git log --format=format:%s -1")}""" + env.OLD_SEMANTIC_VERSION="""${sh( + returnStdout: true, + script: ''' + set +x; + cog -v get-version --fallback 0.1.0 + ''' + )}""" + } + } + } + + stage('Versioning Management') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh 'cog bump --auto --skip-ci "[skip ci]"' + } + } + + stage('Pushing release commit and tags') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + sh "git push origin HEAD:${BRANCH_NAME}" + sh "git push origin --tags" + + } + } + } + + stage('Get New Version') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + env.COMMIT_HASH = """${sh(returnStdout: true, script: "git rev-parse --short HEAD")}""".trim() + env.NEW_SEMANTIC_VERSION="""${sh( + returnStdout: true, + script: ''' + set +x; + cog -v get-version + ''' + )}""" + echo "New version - ${NEW_SEMANTIC_VERSION}, Old version - ${OLD_SEMANTIC_VERSION}" + } + } + } + stage('Build Image') { when { expression { SKIP_CI == 'false' } - branch 'main' + expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + branch 'main' } steps { sh 'make ci-build -e VERSION=${COMMIT_HASH}' } } @@ -42,10 +138,11 @@ pipeline { stage('Push Image To Sandbox Registry') { when { expression { SKIP_CI == 'false' } - branch 'main' + expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + branch 'main' } steps { - sh '''make ci-push -e \ + sh '''make ci-push -e \ VERSION=${COMMIT_HASH} \ REGION=${REGION} \ REGISTRY_HOST=${REGISTRY_HOST_SBX} @@ -56,6 +153,7 @@ pipeline { stage('Push Image To Production Registry') { when { expression { SKIP_CI == 'false' } + expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } branch 'main' } steps { @@ -70,7 +168,8 @@ pipeline { stage('Create Integ Release Tracker') { when { expression { SKIP_CI == 'false' } - branch 'main' + expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + branch 'main' } environment { CREDS = credentials('AP_INTEG_ID') diff --git a/cog.toml b/cog.toml new file mode 100644 index 000000000..dd10ed61f --- /dev/null +++ b/cog.toml @@ -0,0 +1,33 @@ +from_latest_tag = true +ignore_merge_commits = false +branch_whitelist = [] + +tag_prefix = "v" +monorepo_version_separator = "-" + +pre_bump_hooks = [] + +pre_package_bump_hooks = [ + "echo 'upgrading {{package}}' to {{version}}", + "cargo set-version --package {{package}} {{version}}" +] + +post_package_bump_hooks = [] + +post_bump_hooks = [] + +[commit_types] + +[changelog] +path = "CHANGELOG.md" +authors = [] + +[bump_profiles] + +[packages] +context-aware-config = { path = "crates/context-aware-config" } +experimentation-platform = { path = "crates/experimentation-platform" } +service-utils = { path = "crates/service-utils" } + +cac_client = { path = "crates/cac_client" } +superposition_client = { path = "crates/superposition_client" } From 6b06dfecab997dabd32bb6fde3731765c91abd2a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 24 Aug 2023 18:28:35 +0530 Subject: [PATCH 099/352] build: added version tag to docker images --- Dockerfile | 2 ++ Jenkinsfile | 49 ++++++++++++++++++------------------------------- makefile | 6 +++++- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36c84cf6f..56d5be216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ FROM debian:bullseye RUN apt-get update RUN apt-get install --yes libpq5 ca-certificates COPY --from=builder /app/target/release/context-aware-config /app/context-aware-config +ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION +ENV SOURCE_COMMIT=$SOURCE_COMMIT WORKDIR /app CMD ["/app/context-aware-config"] diff --git a/Jenkinsfile b/Jenkinsfile index 94241b365..bdf335ccb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,33 +35,11 @@ pipeline { } } - stage('Get Git Creds') { - steps { - script { - sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' - sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' - sh 'git fetch' - sh 'git config user.name ""Jenkins User""' - sh 'git config user.email bitbucket.jenkins.read@juspay.in' - } - } - } - stage('Test') { when { expression { SKIP_CI == 'false' } } steps { sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' } } - stage('Cargo Install') { - when { - expression { SKIP_CI == 'false' } - branch 'main' - } - steps { - sh 'cargo install cocogitto@5.5.0 cargo-edit' - } - } - stage('Get old Version') { when { expression { SKIP_CI == 'false' } @@ -69,7 +47,6 @@ pipeline { } steps { script { - env.BUILD_PIPELINE_RUNNING=true env.COMMIT_MSG="""${sh(returnStdout: true, script: "git log --format=format:%s -1")}""" env.OLD_SEMANTIC_VERSION="""${sh( returnStdout: true, @@ -99,9 +76,14 @@ pipeline { } steps { script { + sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' + sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' + sh 'git fetch' + sh 'git config user.name ""Jenkins User""' + sh 'git config user.email bitbucket.jenkins.read@juspay.in' + sh "git push origin HEAD:${BRANCH_NAME}" sh "git push origin --tags" - } } } @@ -129,21 +111,26 @@ pipeline { stage('Build Image') { when { expression { SKIP_CI == 'false' } - expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } branch 'main' } - steps { sh 'make ci-build -e VERSION=${COMMIT_HASH}' } + steps { + sh '''make ci-build -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + SOURCE_COMMIT=${COMMIT_HASH} + ''' + } } stage('Push Image To Sandbox Registry') { when { expression { SKIP_CI == 'false' } - expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } branch 'main' } steps { sh '''make ci-push -e \ - VERSION=${COMMIT_HASH} \ + VERSION=${NEW_SEMANTIC_VERSION} \ REGION=${REGION} \ REGISTRY_HOST=${REGISTRY_HOST_SBX} ''' @@ -153,12 +140,12 @@ pipeline { stage('Push Image To Production Registry') { when { expression { SKIP_CI == 'false' } - expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } branch 'main' } steps { sh '''make ci-push -e \ - VERSION=${COMMIT_HASH} \ + VERSION=${NEW_SEMANTIC_VERSION} \ REGION=${REGION} \ REGISTRY_HOST=${REGISTRY_HOST_PROD} ''' @@ -168,7 +155,7 @@ pipeline { stage('Create Integ Release Tracker') { when { expression { SKIP_CI == 'false' } - expression { NEW_SEMANTIC_VERSION != OLD_SEMANTIC_VERSION } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } branch 'main' } environment { diff --git a/makefile b/makefile index 2628e954c..7536df4ac 100644 --- a/makefile +++ b/makefile @@ -58,7 +58,11 @@ ci-test: npm run test ci-build: - docker build -t $(IMAGE_NAME):$(VERSION) . + docker build \ + -t $(IMAGE_NAME):$(VERSION) \ + --build-arg "CONTEXT_AWARE_CONFIG_VERSION=${VERSION}" \ + --build-arg "SOURCE_COMMIT=${SOURCE_COMMIT}" \ + . ci-push: registry-login docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY_HOST)/$(IMAGE_NAME):$(VERSION) From f5a3f74ab8baa802b0017a9eaf66a185b56c716c Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 25 Aug 2023 17:50:45 +0530 Subject: [PATCH 100/352] fix: added middleware to insert version in response headers --- .env.example | 1 + crates/context-aware-config/src/main.rs | 9 ++++++++- crates/service-utils/src/db/utils.rs | 3 +-- crates/service-utils/src/service/types.rs | 9 +++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index f1fbd5e6a..723191430 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,4 @@ ALLOW_SAME_KEYS_OVERLAPPING_CTX=true ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" +CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 8c4798ebb..4e788a8e7 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -16,7 +16,7 @@ use std::sync::Mutex; use service_utils::{ db::utils::get_pool, helpers::get_from_env_unsafe, - service::types::{AppState, ExperimentationFlags} + service::types::{AppState, ExperimentationFlags}, }; use api::*; @@ -31,6 +31,8 @@ async fn main() -> Result<()> { let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); + let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") + .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); /****** EXPERIMENTATION PLATFORM ENVs *********/ @@ -54,6 +56,7 @@ async fn main() -> Result<()> { default_config_validation_schema: get_default_config_validation_schema(), admin_token: admin_token.to_owned(), cac_host: cac_host.to_owned(), + cac_version: cac_version.to_owned(), experimentation_flags: ExperimentationFlags { allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx @@ -67,6 +70,10 @@ async fn main() -> Result<()> { snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1, 1)), })) .wrap(logger) + .wrap( + actix_web::middleware::DefaultHeaders::new() + .add(("X-SERVER-VERSION", cac_version.to_string())), + ) .route( "/health", get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index 05d9a00b3..6d7426b0d 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,4 +1,3 @@ - use crate::aws::kms; use crate::helpers::get_from_env_unsafe; use diesel::{ @@ -24,4 +23,4 @@ pub async fn get_pool() -> Pool> { Pool::builder() .build(manager) .expect("Error building a connection pool") -} \ No newline at end of file +} diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 229fc0005..4244a0ff1 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -1,5 +1,5 @@ use diesel::{ - r2d2::{PooledConnection, ConnectionManager, Pool}, + r2d2::{ConnectionManager, Pool, PooledConnection}, PgConnection, }; use jsonschema::JSONSchema; @@ -12,16 +12,17 @@ use snowflake::SnowflakeIdGenerator; use std::sync::Mutex; pub struct ExperimentationFlags { - pub allow_same_keys_overlapping_ctx : bool, - pub allow_diff_keys_overlapping_ctx : bool, + pub allow_same_keys_overlapping_ctx: bool, + pub allow_diff_keys_overlapping_ctx: bool, pub allow_same_keys_non_overlapping_ctx: bool, } pub struct AppState { pub cac_host: String, + pub cac_version: String, + pub admin_token: String, pub db_pool: Pool>, pub default_config_validation_schema: JSONSchema, - pub admin_token: String, pub experimentation_flags: ExperimentationFlags, pub snowflake_generator: Mutex, } From 532a6d2d2c568d6b6765c5c469b44a03898bad7f Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 1 Sep 2023 16:07:10 +0530 Subject: [PATCH 101/352] fix: logged env variable's value before kms decrypting - this is needed to debug decryption failed issue in new sbx NOTE: this change will be REVERTED --- Jenkinsfile | 110 ++++++++++++++-------------- crates/service-utils/src/aws/kms.rs | 4 +- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index bdf335ccb..befdfe92b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -137,61 +137,61 @@ pipeline { } } - stage('Push Image To Production Registry') { - when { - expression { SKIP_CI == 'false' } - expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } - branch 'main' - } - steps { - sh '''make ci-push -e \ - VERSION=${NEW_SEMANTIC_VERSION} \ - REGION=${REGION} \ - REGISTRY_HOST=${REGISTRY_HOST_PROD} - ''' - } - } - - stage('Create Integ Release Tracker') { - when { - expression { SKIP_CI == 'false' } - expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } - branch 'main' - } - environment { - CREDS = credentials('AP_INTEG_ID') - COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") - CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; - AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") - } - steps { - sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Basic ${CREDS_PSW}' \ - --data-raw '{ - "service": ["CONTEXT_AWARE_CONFIG"], - "release_manager": "${AUTHOR_NAME}", - "release_tag": "", - "new_version": "${COMMIT_HASH}", - "docker_image" : "${COMMIT_HASH}", - "priority" : 0, - "cluster" : "INTEG_CLUSTER", - "change_log": "${CHANGE_LOG}", - "rollout_strategy": [ - { - "rollout": 100, - "cooloff": 0, - "pods": 1 - } - ], - "description": "${CHANGE_LOG}", - "product": "HYPER_SDK", - "mode" : "AUTO", - "env" : "INTEG" - }'; - """ - } - } +// stage('Push Image To Production Registry') { +// when { +// expression { SKIP_CI == 'false' } +// expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } +// branch 'main' +// } +// steps { +// sh '''make ci-push -e \ +// VERSION=${NEW_SEMANTIC_VERSION} \ +// REGION=${REGION} \ +// REGISTRY_HOST=${REGISTRY_HOST_PROD} +// ''' +// } +// } +// +// stage('Create Integ Release Tracker') { +// when { +// expression { SKIP_CI == 'false' } +// expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } +// branch 'main' +// } +// environment { +// CREDS = credentials('AP_INTEG_ID') +// COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") +// CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; +// AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") +// } +// steps { +// sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ +// --header 'Content-Type: application/json' \ +// --header 'Authorization: Basic ${CREDS_PSW}' \ +// --data-raw '{ +// "service": ["CONTEXT_AWARE_CONFIG"], +// "release_manager": "${AUTHOR_NAME}", +// "release_tag": "", +// "new_version": "${COMMIT_HASH}", +// "docker_image" : "${COMMIT_HASH}", +// "priority" : 0, +// "cluster" : "INTEG_CLUSTER", +// "change_log": "${CHANGE_LOG}", +// "rollout_strategy": [ +// { +// "rollout": 100, +// "cooloff": 0, +// "pods": 1 +// } +// ], +// "description": "${CHANGE_LOG}", +// "product": "HYPER_SDK", +// "mode" : "AUTO", +// "env" : "INTEG" +// }'; +// """ +// } +// } stage('Summary') { steps { diff --git a/crates/service-utils/src/aws/kms.rs b/crates/service-utils/src/aws/kms.rs index d42c9e132..3076ebd1f 100644 --- a/crates/service-utils/src/aws/kms.rs +++ b/crates/service-utils/src/aws/kms.rs @@ -6,7 +6,9 @@ use rusoto_signature::region::Region; //TODO refactor below code #[allow(deprecated)] pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { - let cypher = get_from_env_unsafe(secret_name) + let cypher = get_from_env_unsafe(secret_name); + log::info!("{secret_name} = {cypher:?}"); + let cypher = cypher .map(|x: String| base64::decode(x).unwrap()) .expect(format!("{secret_name} not found in env").as_str()); let req = DecryptRequest { From bfbc1f567f1eff4801a3be5e6423aa6922d83ad7 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 1 Sep 2023 11:09:58 +0000 Subject: [PATCH 102/352] chore(version): v0.1.0 [skip ci] --- CHANGELOG.md | 83 ++++++++++++++++++++ crates/cac_client/CHANGELOG.md | 11 +++ crates/context-aware-config/CHANGELOG.md | 23 ++++++ crates/experimentation-platform/CHANGELOG.md | 31 ++++++++ crates/service-utils/CHANGELOG.md | 22 ++++++ crates/superposition_client/CHANGELOG.md | 18 +++++ 6 files changed, 188 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 crates/cac_client/CHANGELOG.md create mode 100644 crates/context-aware-config/CHANGELOG.md create mode 100644 crates/experimentation-platform/CHANGELOG.md create mode 100644 crates/service-utils/CHANGELOG.md create mode 100644 crates/superposition_client/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..4b6a02b3e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,83 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## v0.1.0 - 2023-09-01 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.1.0 +- service-utils bumped to service-utils-v0.1.0 +- cac_client bumped to cac_client-v0.1.0 +- context-aware-config bumped to context-aware-config-v0.1.0 +- superposition_client bumped to superposition_client-v0.1.0 +### Global changes +#### Bug Fixes +- PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- moved tables and types under cac_v1 schema - (1be82f1) - Shubhranshu Sanjeev +- added last_modified column and indexes - (942d723) - Shubhranshu Sanjeev +- PICAF-23634 fixed ordering of /context endpoints - (e7b71b3) - Ritick Madaan +- PICAF-22642 moved dimension_type to cac_v1 schema - (5ae90a7) - Ritick Madaan +- PICAF-23520 enabled override updates in PUT /context by deep_merge - (53b148a) - Ritick Madaan +- PICAF-23552 added moved tables to cac_v1 schema - (f9fb191) - Ritick Madaan +- PICAF-23552 database schema url - (e32c1c7) - Ritick Madaan +- PICAF-23552 added search path for schema in database_url - (fd36bdc) - Ritick Madaan +- PICAF-23199 removed unecessary and wrap over conditions - (602643d) - Ritick Madaan +- PICAF-23231 moved override inside contexts table - (e09dd45) - Ritick Madaan +- PICAF-22967: DB password URI encoded. - (c01d921) - Shrey Bana +- removed delete dimension api - (b3b126b) - Ritick Madaan +- storing pre calculated priority in contexts table - (7de363c) - Ritick Madaan +- PICAF-22646 dimension Table scaffolding, PUT<>DELETE apis `/dimension` - (6f4b52e) - Ritick Madaan +- changed host to 0.0.0.0 - (f487a5b) - Ritick Madaan +- base64 decoding kms cypher - (952b546) - Ritick Madaan +- improved log for env not found - (7240266) - Ritick Madaan +- corrected env for DB_PASSWORD and default for AWS_REGION - (a056d85) - Ritick Madaan +#### Build system +- added version tag to docker images - (501bdf4) - Shubhranshu Sanjeev +- PICAF-22783 installing ca-certificates for ssl verification - (626e8e5) - Ritick Madaan +#### Continuous Integration +- added cocogitto config for automatic versioning - (1266bf9) - Shubhranshu Sanjeev +- added postman collection for experimentation-platform - (280441a) - Shubhranshu Sanjeev +- fix newman dev dependency ref - (11e4cbd) - Natarajan Kannan +- PICAF-23646 udpated `docker container ls` filter - (ce93bc0) - Ritick Madaan +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +- PICAF-23646: switch to using newman - (846b931) - Natarajan Kannan +- automated newman test setup - (c2667c0) - Natarajan Kannan +- PICAF-22647: Upgraded nixos to 23.05. - (267dfdc) - Shrey Bana +- moved nixpkgs to nixos-22.11 as the unstable one had broken rustfmt - (0083af7) - Ritick Madaan +- PICAF-22654: Enabling production docker image push. - (76311d7) - Shrey Bana +- PICAF-22654: Commented out prod docker push. - (60b8142) - Shrey Bana +- PICAF-22654: Created pipeline for automated-deployment - (b3208cc) - Shrey Bana +- made some miscellaneous changes for local setup - (89cf0ec) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- [PICAF-24010] added support for CUG in super position client - (4eeae99) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +- added 304 <> last-modified for GET /config - (7592a21) - Saurav Suman +- PICAF-23520 PUT /context/move/{ctx_id} api - (4875284) - Ritick Madaan +- PICAF-22511 added DELETE /context/{ctx_id} api - (d557f84) - Ritick Madaan +- PICAF-22678: Added authentication. - (cd20874) - Shrey Bana +- PICAF-23199 removed properties constraint on objects in schema - (dd83bd5) - Ritick Madaan +- PICAF-23057 rust library - (05f80ec) - Ritick Madaan +- PICAF-22932: Added support for validation via JSON schema. - (51c81cb) - Shrey Bana +- PICAF-22664: Added context list API - (dc21416) - Shrey Bana +- GET /config api - (37ab8e9) - Ritick Madaan +- /default-config/ PUT api - (ae12c7b) - Ritick Madaan +- PICAF-22783 added localstack setup along with kms - (a8d3a15) - Ritick Madaan +- PICAF-22654: Added health-check endpoint. - (993a6d2) - Shrey Bana +- PICAF-22510 Added context fetch API - (7609a68) - Shrey Bana +- context/add api along with db setup - (f5206cb) - Ritick Madaan +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev +- moved AppState & utility fx to new crate - (4f734a5) - Shubhranshu Sanjeev +- PICAF-22469 removed old contexts table - (49770dc) - Ritick Madaan +- PICAF-22468 moved db related modules to db crate - (f7d9492) - Ritick Madaan +#### Tests +- update to latest newman that handles top level events and body lang type - (fcd7724) - Natarajan Kannan +- fix newman version used in tests - (62ede3d) - Natarajan Kannan + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md new file mode 100644 index 000000000..b3c4a1b13 --- /dev/null +++ b/crates/cac_client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## cac_client-v0.1.0 - 2023-09-01 +#### Features +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md new file mode 100644 index 000000000..93ec47e09 --- /dev/null +++ b/crates/context-aware-config/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## context-aware-config-v0.1.0 - 2023-09-01 +#### Bug Fixes +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- PICAF-24023 sorting same priority contexts with created_at - (24852f5) - Ritick Madaan +- using audit log tstamp for checking last-modified - (2ccaa7e) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- PICAF-23545 updated response-type of /context/bulk-operations api - (1640986) - Ritick Madaan +#### Continuous Integration +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +#### Features +- added log table for all cac_v1 tables - (88a3328) - Shubhranshu Sanjeev +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md new file mode 100644 index 000000000..f35ff1967 --- /dev/null +++ b/crates/experimentation-platform/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## experimentation-platform-v0.1.0 - 2023-09-01 +#### Bug Fixes +- using audit log tstamp for checking last-modified - (2ccaa7e) - Shubhranshu Sanjeev +- [PICAF-23846] added total items to list API response - (17955fa) - Kartik Gajendra +- removed traffic-percentage from experiment create request - (2a62555) - Shubhranshu Sanjeev +- PICAF-23632 - (247542e) - Ritick Madaan +- PICAF-23622 updated last_modified in ramp - (fcbaaa4) - ankit.mahato +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- moved tables and types under cac_v1 schema - (1be82f1) - Shubhranshu Sanjeev +- added last_modified column and indexes - (942d723) - Shubhranshu Sanjeev +- fixed context overlap check logic - (691eae7) - Shubhranshu Sanjeev +#### Continuous Integration +- regenerated schema.patch with latest schema.rs - (390818a) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (91386ee) - Kartik Gajendra +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- added log table for all cac_v1 tables - (88a3328) - Shubhranshu Sanjeev +- [PICAF-23856] add support for last - (d23ee26) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +- added conclude functionality for experiments - (4def4bc) - Shubhranshu Sanjeev +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md new file mode 100644 index 000000000..68316cd3f --- /dev/null +++ b/crates/service-utils/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## service-utils-v0.1.0 - 2023-09-01 +#### Bug Fixes +- PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +#### Refactoring +- moved fetching db connection in FromRequest trait impl - (c07c1d2) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev +- moved AppState & utility fx to new crate - (4f734a5) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md new file mode 100644 index 000000000..b6194bd89 --- /dev/null +++ b/crates/superposition_client/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## superposition_client-v0.1.0 - 2023-09-01 +#### Bug Fixes +- PICAF-24114 removed unwanted parameter to prevent warning - (3de7fe7) - Ritick Madaan +- PICAF-24114 allowing cug users to fall under test variants - (c095333) - Ritick Madaan +- [PICAF-23846] added total items to list API response - (17955fa) - Kartik Gajendra +- PICAF-23632 - (247542e) - Ritick Madaan +- [PICAF-23632] minor fixes for exp client - (64deee5) - Kartik Gajendra +#### Features +- [PICAF-24010] added support for CUG in super position client - (4eeae99) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file From 154a601ec976d76d3cf180091e860c625c6e53f9 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 1 Sep 2023 17:31:28 +0530 Subject: [PATCH 103/352] fix: moved git init to separate stage --- Jenkinsfile | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index befdfe92b..7b0e43396 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,6 +35,18 @@ pipeline { } } + stage('Git init') { + steps { + script { + sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' + sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' + sh 'git fetch' + sh 'git config user.name ""Jenkins User""' + sh 'git config user.email bitbucket.jenkins.read@juspay.in' + } + } + } + stage('Test') { when { expression { SKIP_CI == 'false' } } steps { sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' } @@ -76,12 +88,6 @@ pipeline { } steps { script { - sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' - sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' - sh 'git fetch' - sh 'git config user.name ""Jenkins User""' - sh 'git config user.email bitbucket.jenkins.read@juspay.in' - sh "git push origin HEAD:${BRANCH_NAME}" sh "git push origin --tags" } @@ -201,4 +207,4 @@ pipeline { } } } -} +} \ No newline at end of file From 664770b3513000a4de8fe1d52dc24e53683c487c Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 1 Sep 2023 18:11:18 +0530 Subject: [PATCH 104/352] fix: cleaned up Dockerfile to fix the following error: /app/context-aware-config: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory --- Dockerfile | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 56d5be216..b1ad45f63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,12 @@ -FROM rust as planner -WORKDIR /app -RUN cargo install cargo-chef +FROM rust:1.67 as builder +WORKDIR /build COPY . . -RUN cargo chef prepare --recipe-path recipe.json - -FROM rust as cacher -WORKDIR /app -RUN cargo install cargo-chef -COPY --from=planner /app/recipe.json recipe.json -RUN cargo chef cook --release --recipe-path recipe.json - -FROM rust as builder -COPY . /app -WORKDIR /app -COPY --from=cacher /app/target target -COPY --from=cacher /usr/local/cargo /usr/local/cargo RUN cargo build --release -FROM debian:bullseye -RUN apt-get update -RUN apt-get install --yes libpq5 ca-certificates -COPY --from=builder /app/target/release/context-aware-config /app/context-aware-config +FROM debian:bullseye-slim +WORKDIR /app +RUN apt-get update && apt-get install -y libpq5 ca-certificates +COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT -WORKDIR /app - CMD ["/app/context-aware-config"] From 551968e9d90753f99efbee415e92dea2e72341bc Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 28 Aug 2023 17:19:04 +0530 Subject: [PATCH 105/352] feat: implemented tracing-actix-web for logging --- Cargo.lock | 307 +++++++++++++++++- crates/context-aware-config/Cargo.toml | 9 +- .../src/api/config/handlers.rs | 3 +- crates/context-aware-config/src/helpers.rs | 21 ++ crates/context-aware-config/src/logger.rs | 162 ++++++--- crates/context-aware-config/src/main.rs | 16 +- 6 files changed, 467 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2101b5ba9..61d523a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,7 @@ name = "context-aware-config" version = "0.1.0" dependencies = [ "actix", + "actix-http", "actix-web", "base64 0.21.2", "blake3", @@ -700,6 +701,7 @@ dependencies = [ "dotenv", "env_logger", "experimentation-platform", + "futures 0.3.28", "json-patch", "jsonschema", "log", @@ -713,8 +715,13 @@ dependencies = [ "service-utils", "strum", "strum_macros", + "tracing", + "tracing-actix-web", + "tracing-log", + "tracing-subscriber", "urlencoding", "uuid 0.8.2", + "valuable", ] [[package]] @@ -1071,6 +1078,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "erased-serde" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.1" @@ -1903,11 +1919,21 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ - "cfg-if 1.0.0", + "serde", + "value-bag", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", ] [[package]] @@ -2070,6 +2096,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi 0.3.9", +] + [[package]] name = "num" version = "0.4.0" @@ -2227,6 +2263,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.9.0" @@ -2294,6 +2336,26 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2554,9 +2616,24 @@ checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.1", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.1" @@ -2928,6 +3005,15 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.104" @@ -3009,6 +3095,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.1.0" @@ -3117,6 +3212,74 @@ dependencies = [ "tokio 1.29.1", ] +[[package]] +name = "sval" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" + +[[package]] +name = "sval_buffer" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +dependencies = [ + "itoa 1.0.6", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +dependencies = [ + "itoa 1.0.6", + "ryu", + "sval", +] + +[[package]] +name = "sval_ref" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +dependencies = [ + "serde", + "sval", + "sval_buffer", + "sval_fmt", +] + [[package]] name = "syn" version = "1.0.109" @@ -3194,6 +3357,16 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + [[package]] name = "time" version = "0.1.45" @@ -3460,9 +3633,33 @@ dependencies = [ "cfg-if 1.0.0", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0b08ce08cbde6a96fc1e4ebb8132053e53ec7a5cd27eef93ede6b73ebbda06" +dependencies = [ + "actix-web", + "pin-project", + "tracing", + "uuid 1.3.4", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -3470,6 +3667,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec 1.10.0", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -3608,6 +3848,65 @@ name = "uuid" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +dependencies = [ + "valuable-derive", +] + +[[package]] +name = "valuable-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d44690c645190cfce32f91a1582281654b2338c6073fa250b0949fd25c55b32" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] [[package]] name = "vcpkg" diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 1edf0a1df..828c8ab0a 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -22,7 +22,7 @@ serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} # For logging and debugging env_logger = "0.8" -log = "^0.4" +log = { version="0.4.20", features = ["kv_unstable_serde"] } # to work with enums strum_macros = "^0.24" strum = {version = "^0.24"} @@ -45,3 +45,10 @@ json-patch = "1.0.0" service-utils = { path = "../service-utils" } experimentation-platform = { path = "../experimentation-platform"} +tracing-actix-web = "0.7.6" +tracing-log = "0.1.3" +tracing = { version = "0.1.37", features = ["valuable"]} +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"]} +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +futures = "0.3.28" +actix-http = "3.3.1" diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 9f89cda4f..bd7defb46 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -2,8 +2,7 @@ use std::collections::HashMap; use super::types::Config; use crate::db::schema::cac_v1::{ - contexts::dsl as ctxt, default_configs::dsl as def_conf, - event_log::dsl as event_log, + contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; use actix_web::{error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope}; use chrono::{DateTime, NaiveDateTime, Utc}; diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index dbe00f3ef..0e2591253 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -1,5 +1,7 @@ +use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; use jsonschema::{Draft, JSONSchema}; use serde_json::json; +use std::collections::HashMap; pub fn get_default_config_validation_schema() -> JSONSchema { let my_schema = json!({ @@ -38,3 +40,22 @@ pub fn get_default_config_validation_schema() -> JSONSchema { .compile(&my_schema) .expect("THE IMPOSSIBLE HAPPENED, failed to compile the schema for the schema!") } + +pub fn parse_headermap_safe(headermap: &HeaderMap) -> HashMap { + let mut req_headers = HashMap::new(); + let record_header = |(header_name, header_val): (&HeaderName, &HeaderValue)| { + let header_val = match header_val.to_str() { + Ok(s) => String::from(s), + Err(e) => { + log::error!( + "unable to parse value of header {}, error: {e}", + header_name + ); + String::from("Error: non ASCII header value") + } + }; + req_headers.insert(header_name.to_string(), header_val); + }; + headermap.iter().for_each(record_header); + req_headers +} diff --git a/crates/context-aware-config/src/logger.rs b/crates/context-aware-config/src/logger.rs index 275cc2818..00ff4caf8 100644 --- a/crates/context-aware-config/src/logger.rs +++ b/crates/context-aware-config/src/logger.rs @@ -1,44 +1,132 @@ -use env_logger::{Builder, Env}; -use serde::{ser::SerializeStruct, Serialize, Serializer}; +use crate::helpers::parse_headermap_safe; +use actix_web::{ + body::MessageBody, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + web::BytesMut, + Error, HttpMessage, +}; +use futures::future::{ready, LocalBoxFuture, Ready}; +use futures::{FutureExt, StreamExt}; use serde_json::{json, Value}; -use std::io::Write; - -//TODO make Log type more strict and have custom implementation of -//serialize trait -pub struct Log { - value: Value, - timestamp: String, - level: String, +use std::{cell::RefCell, rc::Rc}; +use tracing::{span, Level, Span}; +use tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder}; +use tracing_subscriber::filter::EnvFilter; + +//---------------- GoldenSignal Middleware ----------------- + +// There are two steps in middleware processing. +// 1. Middleware initialization, middleware factory gets called with +// next service in chain as parameter. +// 2. Middleware's call method gets called with normal request. + +pub struct GoldenSignalFactory; + +// Middleware factory is `Transform` trait +// `S` - type of the next service +// `B` - type of response's body +impl Transform for GoldenSignalFactory +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static + std::fmt::Debug, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = GoldenSignal; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(GoldenSignal { + service: Rc::new(RefCell::new(service)), + })) + } } -//NOTE we can't use '-' in a struct identifier name therefore writing custom serializer -impl Serialize for Log { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let no_of_fields = 4; - let mut log = serializer.serialize_struct("Log", no_of_fields)?; - log.serialize_field("value", &self.value)?; - log.serialize_field("service", "context-aware-config")?; - log.serialize_field("timestamp", &self.timestamp)?; - log.serialize_field("level", &self.level)?; - log.end() +pub struct GoldenSignal { + service: Rc>, +} + +impl Service for GoldenSignal +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static + std::fmt::Debug, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let svc = self.service.clone(); + + async move { + let mut body = BytesMut::new(); + let mut payload = req.take_payload(); + while let Some(chunk) = payload.next().await { + let chunk = chunk?; + body.extend_from_slice(&chunk); + } + + let (_payload_sender, mut payload) = actix_http::h1::Payload::create(true); + payload.unread_data(body.clone().into()); + req.set_payload(payload.into()); + + let req_headers = parse_headermap_safe(req.headers()); + let res = svc.call(req).await?; + let res_headers = parse_headermap_safe(res.headers()); + let res_body = res.response().body(); + let req_body = String::from_utf8(body.freeze().to_vec()) + .and_then(|s| { + Ok(serde_json::from_str::(s.as_str()) + .unwrap_or(Value::Null)) + }) + .unwrap_or(Value::Null); + + tracing::info!( + request_body = format!("{}", req_body), + request_headers = format!("{}", json!(req_headers)), + reponse_body = format!("{:?}", res_body), + response_headers = format!("{}", json!(res_headers)), + http.status_code = res.status().as_u16(), + "GoldenSignal" + ); + Ok(res) + } + .boxed_local() } } -pub fn init_logger() { - let env = Env::default(); - Builder::from_env(env) - .format(move |buf, record| { - let log = Log { - timestamp: buf.timestamp_millis().to_string(), - value: json!(record.args()), - level: record.level().to_string(), - }; - - writeln!(buf, "{}", serde_json::json!(log)) - }) - .target(env_logger::Target::Stdout) - .init(); +//---------------- GoldenSignal Middleware ----------------- + +pub struct CustomRootSpanBuilder; + +impl RootSpanBuilder for CustomRootSpanBuilder { + fn on_request_start(request: &ServiceRequest) -> Span { + tracing_actix_web::root_span!(request, service = "context-aware-config") + } + + fn on_request_end( + span: Span, + outcome: &Result, Error>, + ) { + DefaultRootSpanBuilder::on_request_end(span, outcome); + let cac_span = span!(Level::INFO, "app", service = "context-aware-config"); + let _span_entered = cac_span.enter(); + } +} + +//let custom_middleware = TracingLogger::::new(); + +pub fn init_log_subscriber() { + let subscriber = tracing_subscriber::fmt::Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()); + if Ok(String::from("DEV")) == std::env::var("APP_ENV") { + subscriber.compact().init(); + } else { + subscriber.json().init(); + } } diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 4e788a8e7..284f81c3e 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -4,14 +4,14 @@ mod helpers; mod logger; use dotenv; -use logger::init_logger; +use logger::{init_log_subscriber, CustomRootSpanBuilder}; use std::{env, io::Result}; +use tracing::{span, Level}; -use actix_web::{ - middleware::Logger, web::get, web::scope, web::Data, App, HttpResponse, HttpServer, -}; +use actix_web::{web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use snowflake::SnowflakeIdGenerator; use std::sync::Mutex; +use tracing_actix_web::TracingLogger; use service_utils::{ db::utils::get_pool, @@ -27,7 +27,9 @@ use experimentation_platform::api::*; #[actix_web::main] async fn main() -> Result<()> { dotenv::dotenv().ok(); - init_logger(); + init_log_subscriber(); + let cac_span = span!(Level::INFO, "app", service = "context-aware-config"); + let _span_entered = cac_span.enter(); let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); @@ -49,8 +51,9 @@ async fn main() -> Result<()> { /****** EXPERIMENTATION PLATFORM ENVs *********/ HttpServer::new(move || { - let logger: Logger = Logger::default(); App::new() + .wrap(logger::GoldenSignalFactory) + .wrap(TracingLogger::::new()) .app_data(Data::new(AppState { db_pool: pool.clone(), default_config_validation_schema: get_default_config_validation_schema(), @@ -69,7 +72,6 @@ async fn main() -> Result<()> { snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1, 1)), })) - .wrap(logger) .wrap( actix_web::middleware::DefaultHeaders::new() .add(("X-SERVER-VERSION", cac_version.to_string())), From f3fd257144585b9920ae0ab930801b09764d91bb Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 5 Sep 2023 11:27:47 +0000 Subject: [PATCH 106/352] chore(version): v0.2.0 [skip ci] --- CHANGELOG.md | 12 ++++ Cargo.lock | 84 ++++++++++++------------ crates/context-aware-config/CHANGELOG.md | 6 ++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b6a02b3e..660c6a030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.2.0 - 2023-09-05 +### Package updates +- context-aware-config bumped to context-aware-config-v0.2.0 +### Global changes +#### Bug Fixes +- PICAF-24273 cleaned up Dockerfile - (cb5c4fb) - Ritick Madaan +- moved git init to separate stage - (9477de6) - Shubhranshu Sanjeev +#### Features +- PICAF-23598 implemented tracing-actix-web for logging - (63dee8c) - Ritick Madaan + +- - - + ## v0.1.0 - 2023-09-01 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 61d523a34..c606f85f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix", "actix-http", @@ -705,7 +705,7 @@ dependencies = [ "json-patch", "jsonschema", "log", - "reqwest 0.11.18", + "reqwest 0.9.24", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -890,6 +890,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1597,15 +1606,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" dependencies = [ - "http 0.2.9", - "hyper 0.14.26", + "bytes 0.4.12", + "ct-logs", + "futures 0.1.31", + "hyper 0.12.36", "rustls", - "tokio 1.29.1", + "tokio-io", "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -2655,11 +2668,13 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", + "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", + "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2667,10 +2682,12 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", + "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", + "webpki-roots", "winreg 0.6.2", ] @@ -2689,7 +2706,6 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", - "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2699,20 +2715,16 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", - "rustls", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", - "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg 0.10.1", ] @@ -2860,33 +2872,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ + "base64 0.10.1", "log", "ring", - "rustls-webpki", "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.100.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" -dependencies = [ - "ring", - "untrusted", + "webpki", ] [[package]] @@ -2933,9 +2927,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -3543,12 +3537,16 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", "rustls", - "tokio 1.29.1", + "tokio-io", + "webpki", ] [[package]] @@ -4031,9 +4029,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", @@ -4041,9 +4039,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" dependencies = [ "webpki", ] diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 93ec47e09..4aff1f808 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.2.0 - 2023-09-05 +#### Features +- PICAF-23598 implemented tracing-actix-web for logging - (63dee8c) - Ritick Madaan + +- - - + ## context-aware-config-v0.1.0 - 2023-09-01 #### Bug Fixes - added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 828c8ab0a..a0229c538 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From a7c24d032562187a071a999858efaaa789145d9a Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 5 Sep 2023 17:25:58 +0530 Subject: [PATCH 107/352] revert: Revert "fix: logged env variable's value before kms decrypting" This reverts commit 532a6d2d2c568d6b6765c5c469b44a03898bad7f. --- Jenkinsfile | 110 ++++++++++++++-------------- crates/service-utils/src/aws/kms.rs | 4 +- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7b0e43396..1f40a7f98 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -143,61 +143,61 @@ pipeline { } } -// stage('Push Image To Production Registry') { -// when { -// expression { SKIP_CI == 'false' } -// expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } -// branch 'main' -// } -// steps { -// sh '''make ci-push -e \ -// VERSION=${NEW_SEMANTIC_VERSION} \ -// REGION=${REGION} \ -// REGISTRY_HOST=${REGISTRY_HOST_PROD} -// ''' -// } -// } -// -// stage('Create Integ Release Tracker') { -// when { -// expression { SKIP_CI == 'false' } -// expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } -// branch 'main' -// } -// environment { -// CREDS = credentials('AP_INTEG_ID') -// COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") -// CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; -// AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") -// } -// steps { -// sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ -// --header 'Content-Type: application/json' \ -// --header 'Authorization: Basic ${CREDS_PSW}' \ -// --data-raw '{ -// "service": ["CONTEXT_AWARE_CONFIG"], -// "release_manager": "${AUTHOR_NAME}", -// "release_tag": "", -// "new_version": "${COMMIT_HASH}", -// "docker_image" : "${COMMIT_HASH}", -// "priority" : 0, -// "cluster" : "INTEG_CLUSTER", -// "change_log": "${CHANGE_LOG}", -// "rollout_strategy": [ -// { -// "rollout": 100, -// "cooloff": 0, -// "pods": 1 -// } -// ], -// "description": "${CHANGE_LOG}", -// "product": "HYPER_SDK", -// "mode" : "AUTO", -// "env" : "INTEG" -// }'; -// """ -// } -// } + stage('Push Image To Production Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_PROD} + ''' + } + } + + stage('Create Integ Release Tracker') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + environment { + CREDS = credentials('AP_INTEG_ID') + COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") + CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; + AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") + } + steps { + sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Basic ${CREDS_PSW}' \ + --data-raw '{ + "service": ["CONTEXT_AWARE_CONFIG"], + "release_manager": "${AUTHOR_NAME}", + "release_tag": "", + "new_version": "${COMMIT_HASH}", + "docker_image" : "${COMMIT_HASH}", + "priority" : 0, + "cluster" : "INTEG_CLUSTER", + "change_log": "${CHANGE_LOG}", + "rollout_strategy": [ + { + "rollout": 100, + "cooloff": 0, + "pods": 1 + } + ], + "description": "${CHANGE_LOG}", + "product": "HYPER_SDK", + "mode" : "AUTO", + "env" : "INTEG" + }'; + """ + } + } stage('Summary') { steps { diff --git a/crates/service-utils/src/aws/kms.rs b/crates/service-utils/src/aws/kms.rs index 3076ebd1f..d42c9e132 100644 --- a/crates/service-utils/src/aws/kms.rs +++ b/crates/service-utils/src/aws/kms.rs @@ -6,9 +6,7 @@ use rusoto_signature::region::Region; //TODO refactor below code #[allow(deprecated)] pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { - let cypher = get_from_env_unsafe(secret_name); - log::info!("{secret_name} = {cypher:?}"); - let cypher = cypher + let cypher = get_from_env_unsafe(secret_name) .map(|x: String| base64::decode(x).unwrap()) .expect(format!("{secret_name} not found in env").as_str()); let req = DecryptRequest { From 16d6f7bf8175584281ad5b7fd4abd744adeed673 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 16 Aug 2023 13:39:09 +0530 Subject: [PATCH 108/352] feat: add audit log search endpoint --- Cargo.lock | 3 +- crates/context-aware-config/Cargo.toml | 2 +- .../src/api/audit_log/handlers.rs | 63 +++++++++++++++++++ .../src/api/audit_log/mod.rs | 4 ++ .../src/api/audit_log/types.rs | 19 ++++++ crates/context-aware-config/src/api/mod.rs | 1 + crates/context-aware-config/src/db/models.rs | 20 +++++- crates/context-aware-config/src/main.rs | 1 + .../src/api/experiments/handlers.rs | 4 +- .../src/api/experiments/types.rs | 47 +------------- crates/service-utils/src/helpers.rs | 38 ++++++++++- crates/service-utils/src/types.rs | 2 +- docker-compose/postgres/Dockerfile | 1 + postman/cac/.meta.json | 3 +- postman/cac/audit_log/.meta.json | 5 ++ postman/cac/audit_log/list/.event.meta.json | 6 ++ .../cac/audit_log/list/event.prerequest.js | 1 + postman/cac/audit_log/list/event.test.js | 5 ++ postman/cac/audit_log/list/request.json | 19 ++++++ 19 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 crates/context-aware-config/src/api/audit_log/handlers.rs create mode 100644 crates/context-aware-config/src/api/audit_log/mod.rs create mode 100644 crates/context-aware-config/src/api/audit_log/types.rs create mode 100644 postman/cac/audit_log/.meta.json create mode 100644 postman/cac/audit_log/list/.event.meta.json create mode 100644 postman/cac/audit_log/list/event.prerequest.js create mode 100644 postman/cac/audit_log/list/event.test.js create mode 100644 postman/cac/audit_log/list/request.json diff --git a/Cargo.lock b/Cargo.lock index c606f85f8..5be9bfe0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,7 +720,7 @@ dependencies = [ "tracing-log", "tracing-subscriber", "urlencoding", - "uuid 0.8.2", + "uuid 1.3.4", "valuable", ] @@ -3848,6 +3848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index a0229c538..20b12a2cf 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -16,7 +16,7 @@ actix-web = "4.0.0" # To help generate snowflake ids rs-snowflake = "0.6.0" # To help with generating uuids -uuid = {version = "^0.8", features = ["v4","serde"]} +uuid = {version = "1.3.4", features = ["v4", "serde"]} # To serialize and deserialize objects from json serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} diff --git a/crates/context-aware-config/src/api/audit_log/handlers.rs b/crates/context-aware-config/src/api/audit_log/handlers.rs new file mode 100644 index 000000000..6396e51bc --- /dev/null +++ b/crates/context-aware-config/src/api/audit_log/handlers.rs @@ -0,0 +1,63 @@ +use actix_web::{get, web::Query, HttpResponse, Scope}; +use chrono::{Duration, Utc}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::json; +use service_utils::{service::types::DbConnection, types as app}; + +use crate::{api::audit_log::types::AuditQueryFilters, db::models::EventLog}; + +use crate::db::schema::cac_v1::event_log::dsl as event_log; + +pub fn endpoints() -> Scope { + Scope::new("").service(get_audit_logs) +} + +#[get("")] +async fn get_audit_logs( + filters: Query, + db_conn: DbConnection, +) -> app::Result { + let DbConnection(mut conn) = db_conn; + + let query_builder = |filters: &AuditQueryFilters| { + let mut builder = event_log::event_log.into_boxed(); + if let Some(tables) = filters.table.clone() { + builder = builder.filter(event_log::table_name.eq_any(tables.0)); + } + if let Some(actions) = filters.action.clone() { + builder = builder.filter(event_log::action.eq_any(actions.0)); + } + if let Some(username) = filters.username.clone() { + builder = builder.filter(event_log::user_name.eq(username)); + } + let now = Utc::now().naive_utc(); + builder + .filter( + event_log::timestamp + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(event_log::timestamp.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(event_log::timestamp.desc()) + .limit(limit) + .offset(offset); + + let log_count: i64 = count_query.count().get_result(&mut conn)?; + + let logs: Vec = query.load(&mut conn)?; + + let total_pages = (log_count as f64 / limit as f64).ceil() as i64; + + Ok(HttpResponse::Ok().json(json!({ + "total_items": log_count, + "total_pages": total_pages, + "data": logs + }))) +} diff --git a/crates/context-aware-config/src/api/audit_log/mod.rs b/crates/context-aware-config/src/api/audit_log/mod.rs new file mode 100644 index 000000000..5332ce406 --- /dev/null +++ b/crates/context-aware-config/src/api/audit_log/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +mod types; + +pub use handlers::endpoints; \ No newline at end of file diff --git a/crates/context-aware-config/src/api/audit_log/types.rs b/crates/context-aware-config/src/api/audit_log/types.rs new file mode 100644 index 000000000..dd7aa554b --- /dev/null +++ b/crates/context-aware-config/src/api/audit_log/types.rs @@ -0,0 +1,19 @@ +use chrono::NaiveDateTime; +use serde::Deserialize; +use service_utils::helpers::deserialize_stringified_list; + +#[derive(Deserialize, Debug, Clone)] +pub struct StringArgs( + #[serde(deserialize_with = "deserialize_stringified_list")] pub Vec, +); + +#[derive(Debug, Clone, Deserialize)] +pub struct AuditQueryFilters { + pub from_date: Option, + pub to_date: Option, + pub table: Option, + pub action: Option, + pub username: Option, + pub count: Option, + pub page: Option, +} diff --git a/crates/context-aware-config/src/api/mod.rs b/crates/context-aware-config/src/api/mod.rs index 32c456945..d1a8fa68d 100644 --- a/crates/context-aware-config/src/api/mod.rs +++ b/crates/context-aware-config/src/api/mod.rs @@ -2,3 +2,4 @@ pub mod config; pub mod context; pub mod default_config; pub mod dimension; +pub mod audit_log; diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 16f8febc8..e325dd2a9 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -1,6 +1,5 @@ -use crate::db::schema::cac_v1::*; -use chrono::offset::Utc; -use chrono::DateTime; +use crate::db::schema::cac_v1::{contexts, default_configs, dimensions, event_log}; +use chrono::{offset::Utc, DateTime, NaiveDateTime}; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -52,3 +51,18 @@ pub struct DefaultConfig { pub created_at: DateTime, pub created_by: String, } + +#[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(table_name = event_log)] +#[diesel(primary_key(id))] +pub struct EventLog { + pub id: uuid::Uuid, + pub table_name: String, + pub user_name: String, + pub timestamp: NaiveDateTime, + pub action: String, + pub original_data: Option, + pub new_data: Option, + pub query: String, +} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 284f81c3e..1bb5a420b 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -85,6 +85,7 @@ async fn main() -> Result<()> { .service(scope("/dimension").service(dimension::endpoints())) .service(scope("/default-config").service(default_config::endpoints())) .service(scope("/config").service(config::endpoints())) + .service(scope("/audit").service(audit_log::endpoints())) .service(experiments::endpoints()) }) .bind(("0.0.0.0", 8080))? diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index ee3db8699..86a88909e 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -48,11 +48,11 @@ async fn create( db_conn: DbConnection, ) -> app::Result> { use crate::db::schema::cac_v1::experiments::dsl::experiments; + let mut variants = req.variants.to_vec(); let DbConnection(mut conn) = db_conn; let override_keys = &req.override_keys; - let mut variants = req.variants.to_vec(); - + let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index ee978cfc3..d325b3aa0 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,11 +1,7 @@ -use std::fmt; - use chrono::{DateTime, Utc}; -use serde::{ - de::{self, IntoDeserializer}, - Deserialize, Serialize, -}; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use service_utils::helpers::deserialize_stringified_list; use crate::db::models::{self, ExperimentStatusType}; @@ -128,10 +124,9 @@ pub struct ContextPutResp { #[derive(Deserialize, Debug, Clone)] pub struct StatusTypes( #[serde(deserialize_with = "deserialize_stringified_list")] - pub Vec + pub Vec, ); - #[derive(Deserialize, Debug)] pub struct ListFilters { pub status: Option, @@ -141,42 +136,6 @@ pub struct ListFilters { pub count: Option, } - -pub fn deserialize_stringified_list<'de, D, I>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: de::Deserializer<'de>, - I: de::DeserializeOwned, -{ - struct StringVecVisitor(std::marker::PhantomData); - - impl<'de, I> de::Visitor<'de> for StringVecVisitor - where - I: de::DeserializeOwned, - { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string containing comma separated values eg: CREATED,INPROGRESS") - } - - fn visit_str(self, v: &str) -> std::result::Result - where - E: de::Error, - { - let mut query_vector = Vec::new(); - for param in v.split(",") { - let p: I = I::deserialize(param.into_deserializer())?; - query_vector.push(p); - } - Ok(query_vector) - } - } - - deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) -} - #[derive(Deserialize, Debug)] pub struct RampRequest { pub traffic_percentage: u64, diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 6ba9dc560..c2a3a8e00 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -1,4 +1,5 @@ use actix_web::{error::ErrorInternalServerError, Error}; +use serde::de::{self, IntoDeserializer}; use std::{env::VarError, fmt, str::FromStr}; //WARN Do NOT use this fxn inside api requests, instead add the required @@ -46,4 +47,39 @@ where } } -// pub fn db_connection_from_state(state: T) -> +pub fn deserialize_stringified_list<'de, D, I>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: de::Deserializer<'de>, + I: de::DeserializeOwned, +{ + struct StringVecVisitor(std::marker::PhantomData); + + impl<'de, I> de::Visitor<'de> for StringVecVisitor + where + I: de::DeserializeOwned, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a string containing comma separated values eg: CREATED,INPROGRESS", + ) + } + + fn visit_str(self, v: &str) -> std::result::Result + where + E: de::Error, + { + let mut query_vector = Vec::new(); + for param in v.split(",") { + let p: I = I::deserialize(param.into_deserializer())?; + query_vector.push(p); + } + Ok(query_vector) + } + } + + deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) +} diff --git a/crates/service-utils/src/types.rs b/crates/service-utils/src/types.rs index 7ad0bab50..f8e7a8b76 100644 --- a/crates/service-utils/src/types.rs +++ b/crates/service-utils/src/types.rs @@ -1,2 +1,2 @@ -pub type Result = core::result::Result; \ No newline at end of file +pub type Result = core::result::Result; diff --git a/docker-compose/postgres/Dockerfile b/docker-compose/postgres/Dockerfile index 017c4de96..303df06f8 100644 --- a/docker-compose/postgres/Dockerfile +++ b/docker-compose/postgres/Dockerfile @@ -1,3 +1,4 @@ FROM postgres:12-alpine COPY ./db_init.sql /docker-entrypoint-initdb.d/db_init.sql +# CMD ["postgres", "-c", "log_statement=all"] diff --git a/postman/cac/.meta.json b/postman/cac/.meta.json index 5b8cd24d0..7c82c21b9 100644 --- a/postman/cac/.meta.json +++ b/postman/cac/.meta.json @@ -3,6 +3,7 @@ "config", "Default Config", "Dimension", - "Context" + "Context", + "audit_log" ] } diff --git a/postman/cac/audit_log/.meta.json b/postman/cac/audit_log/.meta.json new file mode 100644 index 000000000..d7fe61c4d --- /dev/null +++ b/postman/cac/audit_log/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "list" + ] +} diff --git a/postman/cac/audit_log/list/.event.meta.json b/postman/cac/audit_log/list/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/cac/audit_log/list/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/cac/audit_log/list/event.prerequest.js b/postman/cac/audit_log/list/event.prerequest.js new file mode 100644 index 000000000..9944ca812 --- /dev/null +++ b/postman/cac/audit_log/list/event.prerequest.js @@ -0,0 +1 @@ +// any prerequest js code goes here diff --git a/postman/cac/audit_log/list/event.test.js b/postman/cac/audit_log/list/event.test.js new file mode 100644 index 000000000..7ad11654a --- /dev/null +++ b/postman/cac/audit_log/list/event.test.js @@ -0,0 +1,5 @@ +/* global pm */ + +pm.test('expect response be 200', function () { + pm.response.to.be.ok; +}); \ No newline at end of file diff --git a/postman/cac/audit_log/list/request.json b/postman/cac/audit_log/list/request.json new file mode 100644 index 000000000..d3f30e046 --- /dev/null +++ b/postman/cac/audit_log/list/request.json @@ -0,0 +1,19 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/audit", + "host": [ + "{{host}}" + ], + "path": [ + "audit" + ] + } +} From a347bba02db0ee78382c1aee9cd8ee906eaa9165 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 5 Sep 2023 15:22:56 +0000 Subject: [PATCH 109/352] chore(version): v0.3.0 [skip ci] --- CHANGELOG.md | 13 +++++++++++++ Cargo.lock | 6 +++--- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 8 ++++++++ crates/service-utils/Cargo.toml | 2 +- 8 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 660c6a030..0f25f1393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.3.0 - 2023-09-05 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.2.0 +- context-aware-config bumped to context-aware-config-v0.3.0 +- service-utils bumped to service-utils-v0.2.0 +### Global changes +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra +#### Revert +- Revert "fix: PICAF-24114 logged env variable's value before kms decrypting" - (2a935c9) - Ritick Madaan + +- - - + ## v0.2.0 - 2023-09-05 ### Package updates - context-aware-config bumped to context-aware-config-v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index 5be9bfe0e..4ecce67be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.2.0" +version = "0.3.0" dependencies = [ "actix", "actix-http", @@ -1130,7 +1130,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix", "actix-web", @@ -3045,7 +3045,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 4aff1f808..1c40195a3 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.3.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra + +- - - + ## context-aware-config-v0.2.0 - 2023-09-05 #### Features - PICAF-23598 implemented tracing-actix-web for logging - (63dee8c) - Ritick Madaan diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 20b12a2cf..8b9f4b5c5 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index f35ff1967..cfd24a97a 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.2.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.1.0 - 2023-09-01 #### Bug Fixes - using audit log tstamp for checking last-modified - (2ccaa7e) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 4b75deb61..daaf849c7 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 68316cd3f..b106adefd 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.2.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra +#### Revert +- Revert "fix: PICAF-24114 logged env variable's value before kms decrypting" - (2a935c9) - Ritick Madaan + +- - - + ## service-utils-v0.1.0 - 2023-09-01 #### Bug Fixes - PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 9976c4cbb..b0fdb378d 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 23c9e67afe60072c5c4821f0269869c5f0b4646a Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Thu, 24 Aug 2023 16:48:17 +0530 Subject: [PATCH 110/352] feat: added pod information in response headers and logs --- .env.example | 1 + Cargo.lock | 84 ++++++++++++----------- crates/context-aware-config/Cargo.toml | 2 +- crates/context-aware-config/src/logger.rs | 20 +++++- crates/context-aware-config/src/main.rs | 25 +++++-- crates/service-utils/src/helpers.rs | 15 ++++ 6 files changed, 99 insertions(+), 48 deletions(-) diff --git a/.env.example b/.env.example index 723191430..72300f92e 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,4 @@ ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" +HOSTNAME="---" diff --git a/Cargo.lock b/Cargo.lock index 4ecce67be..a00586896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,7 +705,8 @@ dependencies = [ "json-patch", "jsonschema", "log", - "reqwest 0.9.24", + "rand 0.8.5", + "reqwest 0.11.18", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -890,15 +891,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "cxx" version = "1.0.94" @@ -1606,19 +1598,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.17.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes 0.4.12", - "ct-logs", - "futures 0.1.31", - "hyper 0.12.36", + "futures-util", + "http 0.2.9", + "hyper 0.14.26", "rustls", - "tokio-io", + "tokio 1.29.1", "tokio-rustls", - "webpki", - "webpki-roots", ] [[package]] @@ -2668,13 +2657,11 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", - "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", - "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2682,12 +2669,10 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", - "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", - "webpki-roots", "winreg 0.6.2", ] @@ -2706,6 +2691,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", + "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2715,16 +2701,20 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", + "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg 0.10.1", ] @@ -2872,15 +2862,33 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "base64 0.10.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2927,9 +2935,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3537,16 +3545,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.10.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", "rustls", - "tokio-io", - "webpki", + "tokio 1.29.1", ] [[package]] @@ -4030,9 +4034,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.4" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" dependencies = [ "ring", "untrusted", @@ -4040,9 +4044,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.17.0" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 8b9f4b5c5..dbaa5a4fe 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -42,7 +42,7 @@ urlencoding = "~2.1.2" jsonschema = "~0.17" reqwest = { version = "*", features = [ "rustls-tls" ] } json-patch = "1.0.0" - +rand = "0.8.5" service-utils = { path = "../service-utils" } experimentation-platform = { path = "../experimentation-platform"} tracing-actix-web = "0.7.6" diff --git a/crates/context-aware-config/src/logger.rs b/crates/context-aware-config/src/logger.rs index 00ff4caf8..7250a67e0 100644 --- a/crates/context-aware-config/src/logger.rs +++ b/crates/context-aware-config/src/logger.rs @@ -8,6 +8,7 @@ use actix_web::{ use futures::future::{ready, LocalBoxFuture, Ready}; use futures::{FutureExt, StreamExt}; use serde_json::{json, Value}; +use service_utils::helpers::get_pod_info; use std::{cell::RefCell, rc::Rc}; use tracing::{span, Level, Span}; use tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder}; @@ -92,7 +93,7 @@ where reponse_body = format!("{:?}", res_body), response_headers = format!("{}", json!(res_headers)), http.status_code = res.status().as_u16(), - "GoldenSignal" + "GoldenSignal", ); Ok(res) } @@ -106,7 +107,13 @@ pub struct CustomRootSpanBuilder; impl RootSpanBuilder for CustomRootSpanBuilder { fn on_request_start(request: &ServiceRequest) -> Span { - tracing_actix_web::root_span!(request, service = "context-aware-config") + let (pod_identifier, deployment_id) = get_pod_info(); + tracing_actix_web::root_span!( + request, + service = "context-aware-config", + pod_id = pod_identifier, + deployment_id = deployment_id + ) } fn on_request_end( @@ -114,7 +121,14 @@ impl RootSpanBuilder for CustomRootSpanBuilder { outcome: &Result, Error>, ) { DefaultRootSpanBuilder::on_request_end(span, outcome); - let cac_span = span!(Level::INFO, "app", service = "context-aware-config"); + let (pod_identifier, deployment_id) = get_pod_info(); + let cac_span = span!( + Level::INFO, + "app", + service = "context-aware-config", + pod_id = pod_identifier, + deployment_id = deployment_id + ); let _span_entered = cac_span.enter(); } } diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 1bb5a420b..dc429ed57 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -15,7 +15,7 @@ use tracing_actix_web::TracingLogger; use service_utils::{ db::utils::get_pool, - helpers::get_from_env_unsafe, + helpers::{get_from_env_unsafe, get_pod_info}, service::types::{AppState, ExperimentationFlags}, }; @@ -28,7 +28,14 @@ use experimentation_platform::api::*; async fn main() -> Result<()> { dotenv::dotenv().ok(); init_log_subscriber(); - let cac_span = span!(Level::INFO, "app", service = "context-aware-config"); + let (pod_identifier, deployment_id) = get_pod_info(); + let cac_span = span!( + Level::INFO, + "app", + service = "context-aware-config", + pod_id = pod_identifier, + deployment_id = deployment_id + ); let _span_entered = cac_span.enter(); let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); @@ -36,6 +43,11 @@ async fn main() -> Result<()> { let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let string_to_int = |s: &String| -> i32 { + s.chars() + .map(|i| (i as i32) & rand::random::()) + .fold(0, i32::wrapping_add) + }; /****** EXPERIMENTATION PLATFORM ENVs *********/ let allow_same_keys_overlapping_ctx: bool = @@ -70,11 +82,16 @@ async fn main() -> Result<()> { allow_same_keys_non_overlapping_ctx.to_owned(), }, - snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1, 1)), + snowflake_generator: Mutex::new(SnowflakeIdGenerator::new( + string_to_int(&deployment_id), + string_to_int(&pod_identifier), + )), })) .wrap( actix_web::middleware::DefaultHeaders::new() - .add(("X-SERVER-VERSION", cac_version.to_string())), + .add(("X-SERVER-VERSION", cac_version.to_string())) + .add(("X-DEPLOYMENT-ID", deployment_id.clone())) + .add(("X-POD-ID", pod_identifier.clone())), ) .route( "/health", diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index c2a3a8e00..3072ad17e 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -83,3 +83,18 @@ where deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) } + +pub fn get_pod_info() -> (String, String) { + let hostname: String = get_from_env_unsafe("HOSTNAME").expect("HOSTNAME is not set"); + let tokens = hostname + .split("-") + .map(str::to_string) + .collect::>(); + let mut tokens = tokens.iter().rev(); + let (pod_id, _replica_set, deployment_id) = ( + tokens.next().unwrap().to_owned(), + tokens.next().unwrap().to_owned(), + tokens.next().unwrap().to_owned(), + ); + (pod_id, deployment_id) +} From d76376d35aed335b44ec48e59a68aec56005a36e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 6 Sep 2023 07:41:47 +0000 Subject: [PATCH 111/352] chore(version): v0.4.0 [skip ci] --- CHANGELOG.md | 10 +++ Cargo.lock | 87 ++++++++++++------------ crates/context-aware-config/CHANGELOG.md | 6 ++ crates/context-aware-config/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++ crates/service-utils/Cargo.toml | 2 +- 6 files changed, 66 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f25f1393..00521d73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.4.0 - 2023-09-06 +### Package updates +- context-aware-config bumped to context-aware-config-v0.4.0 +- service-utils bumped to service-utils-v0.3.0 +### Global changes +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + ## v0.3.0 - 2023-09-05 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index a00586896..1b830c55d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix", "actix-http", @@ -706,7 +706,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.11.18", + "reqwest 0.9.24", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -891,6 +891,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1598,16 +1607,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" dependencies = [ - "futures-util", - "http 0.2.9", - "hyper 0.14.26", + "bytes 0.4.12", + "ct-logs", + "futures 0.1.31", + "hyper 0.12.36", "rustls", - "tokio 1.29.1", + "tokio-io", "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -2657,11 +2669,13 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", + "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", + "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2669,10 +2683,12 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", + "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", + "webpki-roots", "winreg 0.6.2", ] @@ -2691,7 +2707,6 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", - "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2701,20 +2716,16 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", - "rustls", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", - "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg 0.10.1", ] @@ -2862,33 +2873,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ + "base64 0.10.1", "log", "ring", - "rustls-webpki", "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" -dependencies = [ - "ring", - "untrusted", + "webpki", ] [[package]] @@ -2935,9 +2928,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -3053,7 +3046,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.2.0" +version = "0.3.0" dependencies = [ "actix", "actix-web", @@ -3545,12 +3538,16 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", "rustls", - "tokio 1.29.1", + "tokio-io", + "webpki", ] [[package]] @@ -4034,9 +4031,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.1" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", @@ -4044,9 +4041,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" dependencies = [ "webpki", ] diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 1c40195a3..057da9285 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.4.0 - 2023-09-06 +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + ## context-aware-config-v0.3.0 - 2023-09-05 #### Features - [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index dbaa5a4fe..c64480943 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index b106adefd..64b58630b 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.3.0 - 2023-09-06 +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + ## service-utils-v0.2.0 - 2023-09-05 #### Features - [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index b0fdb378d..bd5b40825 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From f92d1489b7215a9b64723e984bed4aaa910e3558 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 6 Sep 2023 13:40:13 +0530 Subject: [PATCH 112/352] ci: updated integ AP tracker curl with new version --- Jenkinsfile | 6 +++--- crates/superposition_client/README.md | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1f40a7f98..1786e4301 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -178,8 +178,8 @@ pipeline { "service": ["CONTEXT_AWARE_CONFIG"], "release_manager": "${AUTHOR_NAME}", "release_tag": "", - "new_version": "${COMMIT_HASH}", - "docker_image" : "${COMMIT_HASH}", + "new_version": "${NEW_SEMANTIC_VERSION}", + "docker_image" : "${NEW_SEMANTIC_VERSION}", "priority" : 0, "cluster" : "INTEG_CLUSTER", "change_log": "${CHANGE_LOG}", @@ -207,4 +207,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/crates/superposition_client/README.md b/crates/superposition_client/README.md index e69de29bb..139597f9c 100644 --- a/crates/superposition_client/README.md +++ b/crates/superposition_client/README.md @@ -0,0 +1,2 @@ + + From 056b45baf05289c7ffd56a62effe65e146569171 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Wed, 6 Sep 2023 15:11:00 +0530 Subject: [PATCH 113/352] fix: fixed setting env in docker image --- Dockerfile | 6 +++++- crates/superposition_client/README.md | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b1ad45f63..8f6bf7a5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,12 @@ RUN cargo build --release FROM debian:bullseye-slim WORKDIR /app + +ARG SOURCE_COMMIT +ARG CONTEXT_AWARE_CONFIG_VERSION + RUN apt-get update && apt-get install -y libpq5 ca-certificates COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT -CMD ["/app/context-aware-config"] +CMD ["/app/context-aware-config"] \ No newline at end of file diff --git a/crates/superposition_client/README.md b/crates/superposition_client/README.md index 139597f9c..e69de29bb 100644 --- a/crates/superposition_client/README.md +++ b/crates/superposition_client/README.md @@ -1,2 +0,0 @@ - - From 8c3fb61094b9b617b666795daf6c3f69c94edeef Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 6 Sep 2023 09:51:27 +0000 Subject: [PATCH 114/352] chore(version): v0.4.1 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/superposition_client/CHANGELOG.md | 8 ++++++++ crates/superposition_client/Cargo.toml | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00521d73c..1998a62ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.4.1 - 2023-09-06 +### Package updates +- superposition_client bumped to superposition_client-v0.1.1 +### Global changes +#### Bug Fixes +- fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-24114 updated integ AP tracker curl with new version - (1e0fa5b) - Ritick Madaan + +- - - + ## v0.4.0 - 2023-09-06 ### Package updates - context-aware-config bumped to context-aware-config-v0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 1b830c55d..2c8e6e1d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3196,7 +3196,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "dotenv", diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index b6194bd89..740c8d3af 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.1.1 - 2023-09-06 +#### Bug Fixes +- fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-24114 updated integ AP tracker curl with new version - (1e0fa5b) - Ritick Madaan + +- - - + ## superposition_client-v0.1.0 - 2023-09-01 #### Bug Fixes - PICAF-24114 removed unwanted parameter to prevent warning - (3de7fe7) - Ritick Madaan diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 606a5bd7c..24a51bea2 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] From a237254dc151b60dbda3ec9f8cda673259e2d020 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Mon, 28 Aug 2023 14:35:18 +0530 Subject: [PATCH 115/352] feat: record the chosen variant after conclude --- .../down.sql | 3 +++ .../2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql | 3 +++ .../experimentation-platform/src/api/experiments/handlers.rs | 5 ++++- crates/experimentation-platform/src/api/experiments/types.rs | 4 +++- crates/experimentation-platform/src/db/models.rs | 1 + crates/experimentation-platform/src/db/schema.rs | 1 + postman/experimentation-platform/Conclude/request.json | 2 +- 7 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql diff --git a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql new file mode 100644 index 000000000..5dcf1f5c6 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE cac_v1.experiments +DROP COLUMN chosen_variant; diff --git a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql new file mode 100644 index 000000000..70cddb557 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE cac_v1.experiments +ADD COLUMN chosen_variant TEXT; diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 86a88909e..6d8ac1280 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -157,6 +157,7 @@ async fn create( context: req.context.clone(), variants: serde_json::to_value(variants).unwrap(), last_modified_by: email, + chosen_variant: None, }; let mut inserted_experiments = diesel::insert_into(experiments) @@ -180,7 +181,7 @@ async fn conclude( use crate::db::schema::cac_v1::experiments::dsl; let experiment_id: i64 = path.into_inner(); - let winner_variant_id: String = req.into_inner().winner_variant.to_owned(); + let winner_variant_id: String = req.into_inner().chosen_variant.to_owned(); let DbConnection(mut conn) = db_conn; let experiment: Experiment = dsl::experiments @@ -220,6 +221,7 @@ async fn conclude( }; is_valid_winner_variant = true; + operations.push(ContextAction::MOVE((context_id, context_put_req))); } else { // delete this context @@ -262,6 +264,7 @@ async fn conclude( dsl::status.eq(ExperimentStatusType::CONCLUDED), dsl::last_modified.eq(Utc::now()), dsl::last_modified_by.eq(email), + dsl::chosen_variant.eq(Some(winner_variant_id)) )) .get_result::(&mut conn)?; diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index d325b3aa0..b6992e97d 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -62,6 +62,7 @@ pub struct ExperimentResponse { pub context: Value, pub variants: Value, + pub chosen_variant: Option, } impl From for ExperimentResponse { @@ -79,6 +80,7 @@ impl From for ExperimentResponse { context: experiment.context, variants: experiment.variants, + chosen_variant: experiment.chosen_variant, } } } @@ -94,7 +96,7 @@ pub struct ExperimentsResponse { #[derive(Deserialize, Debug)] pub struct ConcludeExperimentRequest { - pub winner_variant: String, + pub chosen_variant: String, } /********** Context Bulk API Type *************/ diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index 1db0dc4af..6194aae5c 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -33,6 +33,7 @@ pub struct Experiment { pub context: Value, pub variants: Value, pub last_modified_by: String, + pub chosen_variant: Option, } pub type Experiments = Vec; diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index c04704988..e9b42d067 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -233,6 +233,7 @@ pub mod cac_v1 { context -> Json, variants -> Json, last_modified_by -> Text, + chosen_variant -> Nullable, } } diff --git a/postman/experimentation-platform/Conclude/request.json b/postman/experimentation-platform/Conclude/request.json index 2d3272b8f..32f60bff8 100644 --- a/postman/experimentation-platform/Conclude/request.json +++ b/postman/experimentation-platform/Conclude/request.json @@ -20,7 +20,7 @@ } }, "raw_json_formatted": { - "winner_variant": "{{experiment_id}}-control" + "chosen_variant": "{{experiment_id}}-control" } }, "url": { From 53b3143792ce0bec9552c09080112dead1b454b4 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 6 Sep 2023 10:33:08 +0000 Subject: [PATCH 116/352] chore(version): v0.5.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1998a62ac..e4330e606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.5.0 - 2023-09-06 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.3.0 +### Global changes +#### Features +- [PICAF-24160] record the chosen variant after conclude - (1c3c6e6) - Kartik Gajendra + +- - - + ## v0.4.1 - 2023-09-06 ### Package updates - superposition_client bumped to superposition_client-v0.1.1 diff --git a/Cargo.lock b/Cargo.lock index 2c8e6e1d3..d49119719 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.2.0" +version = "0.3.0" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index cfd24a97a..3477d51c7 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.3.0 - 2023-09-06 +#### Features +- [PICAF-24160] record the chosen variant after conclude - (1c3c6e6) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.2.0 - 2023-09-05 #### Features - [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index daaf849c7..68a358c2a 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From a37f5d285cf801d8fef74d1d6b6f69d3dd2598ee Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Wed, 6 Sep 2023 16:12:52 +0530 Subject: [PATCH 117/352] fix: trimming newline character from version string --- Jenkinsfile | 6 +++--- crates/superposition_client/README.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1786e4301..327b136c3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -64,7 +64,7 @@ pipeline { returnStdout: true, script: ''' set +x; - cog -v get-version --fallback 0.1.0 + cog -v get-version | tr -d "\n" ''' )}""" } @@ -106,7 +106,7 @@ pipeline { returnStdout: true, script: ''' set +x; - cog -v get-version + cog -v get-version | tr -d "\n" ''' )}""" echo "New version - ${NEW_SEMANTIC_VERSION}, Old version - ${OLD_SEMANTIC_VERSION}" @@ -207,4 +207,4 @@ pipeline { } } } -} +} \ No newline at end of file diff --git a/crates/superposition_client/README.md b/crates/superposition_client/README.md index e69de29bb..8b1378917 100644 --- a/crates/superposition_client/README.md +++ b/crates/superposition_client/README.md @@ -0,0 +1 @@ + From 47b615e6715a20e9cdf6df3e0b808a3655132e1b Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Wed, 6 Sep 2023 16:28:25 +0530 Subject: [PATCH 118/352] ci: deleting postgres's docker image on every test - to prevent previous build's test data poping up in another build's test --- makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/makefile b/makefile index 7536df4ac..1cdd6728a 100644 --- a/makefile +++ b/makefile @@ -50,6 +50,7 @@ run: ci-test: -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) + -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") npm ci --loglevel=error make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ From cd0cb607462a78df6ed9074cb8fc5788509b6895 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 6 Sep 2023 11:06:53 +0000 Subject: [PATCH 119/352] chore(version): v0.5.1 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/superposition_client/CHANGELOG.md | 6 ++++++ crates/superposition_client/Cargo.toml | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4330e606..5b8b6f4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.5.1 - 2023-09-06 +### Package updates +- superposition_client bumped to superposition_client-v0.1.2 +### Global changes +#### Bug Fixes +- trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev +#### Continuous Integration +- deleting postgres's docker image on every test - (8a198d6) - Ritick Madaan + +- - - + ## v0.5.0 - 2023-09-06 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.3.0 diff --git a/Cargo.lock b/Cargo.lock index d49119719..b4bb726f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3196,7 +3196,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.1.1" +version = "0.1.2" dependencies = [ "chrono", "dotenv", diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index 740c8d3af..41cdbe97a 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.1.2 - 2023-09-06 +#### Bug Fixes +- trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev + +- - - + ## superposition_client-v0.1.1 - 2023-09-06 #### Bug Fixes - fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 24a51bea2..6f2813243 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.1.1" +version = "0.1.2" edition = "2021" [dependencies] From 48fc4adf7e2c76c5f6cac861eec7e1ac92b5bd40 Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Mon, 31 Jul 2023 17:35:40 +0530 Subject: [PATCH 120/352] feat: Schema addition for Dimension values --- .../down.sql | 7 ++ .../up.sql | 10 +++ .../src/api/default_config/handlers.rs | 8 +-- .../src/api/default_config/helpers.rs | 26 ------- .../src/api/default_config/mod.rs | 1 - .../src/api/dimension/handlers.rs | 26 ++++++- .../src/api/dimension/types.rs | 5 +- crates/context-aware-config/src/db/models.rs | 16 +---- crates/context-aware-config/src/db/schema.rs | 12 +--- crates/context-aware-config/src/helpers.rs | 68 ++++++++++++++++++- crates/context-aware-config/src/main.rs | 3 +- crates/service-utils/src/service/types.rs | 1 + postman/cac.postman_collection.json | 2 +- .../Dimension/Create Dimension/request.json | 5 +- ...mentation-platform.postman_collection.json | 8 +-- .../Create Experiment/event.prerequest.js | 8 +-- 16 files changed, 131 insertions(+), 75 deletions(-) create mode 100644 crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql create mode 100644 crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql delete mode 100644 crates/context-aware-config/src/api/default_config/helpers.rs diff --git a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql new file mode 100644 index 000000000..8268e5ac5 --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE cac_v1.dimensions DROP COLUMN schema; +ALTER TABLE cac_v1.dimensions ADD COLUMN type dimension_type NOT NULL DEFAULT 'STRING'; + +UPDATE cac_v1.dimensions SET type = 'NUMBER' WHERE dimension = 'toss'; +UPDATE cac_v1.dimensions SET type = 'NUMBER' WHERE dimension = 'tier'; +UPDATE cac_v1.dimensions SET type = 'BOOL' WHERE dimension = 'internalUser'; diff --git a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql new file mode 100644 index 000000000..8166f50a6 --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +ALTER TABLE cac_v1.dimensions ADD COLUMN schema JSON NOT NULL DEFAULT '{}'; +ALTER TABLE cac_v1.dimensions DROP COLUMN type; + +UPDATE cac_v1.dimensions SET schema = '{"enum":["android","ios","web"],"type":"string"}' WHERE dimension = 'os'; +UPDATE cac_v1.dimensions SET schema = '{"type":"number"}' WHERE dimension = 'toss'; +UPDATE cac_v1.dimensions SET schema = '{"pattern":"^[a-z0-9].*$","type":"string"}' WHERE dimension = 'clientId'; +UPDATE cac_v1.dimensions SET schema = '{"enum":["beta","release","cug"],"type":"string"}' WHERE dimension = 'scope'; +UPDATE cac_v1.dimensions SET schema = '{"type":"boolean"}' WHERE dimension = 'internalUser'; +UPDATE cac_v1.dimensions SET schema = '{"type":"number"}' WHERE dimension = 'tier'; diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 6919da4fa..c576bc7b5 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,7 +1,7 @@ -use super::helpers::validate_schema; use super::types::CreateReq; -use crate::db::{ - models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs, +use crate::{ + db::{models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs}, + helpers::validate_jsonschema }; use actix_web::{ put, @@ -28,7 +28,7 @@ async fn create( let req = request.into_inner(); let schema = Value::Object(req.schema); if let Err(e) = - validate_schema(&state.default_config_validation_schema, schema.to_owned()) + validate_jsonschema(&state.default_config_validation_schema, &schema) { return HttpResponse::BadRequest().body(e); }; diff --git a/crates/context-aware-config/src/api/default_config/helpers.rs b/crates/context-aware-config/src/api/default_config/helpers.rs deleted file mode 100644 index 8ee2119ba..000000000 --- a/crates/context-aware-config/src/api/default_config/helpers.rs +++ /dev/null @@ -1,26 +0,0 @@ -use jsonschema::{JSONSchema, ValidationError}; -use serde_json::Value; - -/* - This step is required because an empty object - is also a valid JSON schema. So added required - validations for the input. -*/ -// TODO: Recursive validation. -pub fn validate_schema( - validation_schema: &JSONSchema, - schema: Value, -) -> Result<(), String> { - let res = match validation_schema.validate(&schema) { - Ok(_) => Ok(()), - Err(e) => { - //TODO: Try & render as json. - let verrors = e.collect::>(); - Err(String::from(format!( - "Bad schema: {:?}", - verrors.as_slice() - ))) - } - }; - res -} diff --git a/crates/context-aware-config/src/api/default_config/mod.rs b/crates/context-aware-config/src/api/default_config/mod.rs index fa13772fb..ebe17b924 100644 --- a/crates/context-aware-config/src/api/default_config/mod.rs +++ b/crates/context-aware-config/src/api/default_config/mod.rs @@ -1,4 +1,3 @@ mod handlers; -mod helpers; mod types; pub use handlers::endpoints; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index d6750bcd2..226d5eb99 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -1,6 +1,7 @@ use crate::{ api::dimension::types::CreateReq, db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, + helpers::validate_jsonschema, }; use actix_web::{ put, @@ -10,6 +11,7 @@ use actix_web::{ use chrono::Utc; use diesel::RunQueryDsl; use service_utils::service::types::{AppState, AuthenticationInfo}; +use jsonschema::{Draft, JSONSchema}; pub fn endpoints() -> Scope { Scope::new("").service(create) @@ -28,10 +30,28 @@ async fn create( let AuthenticationInfo(email) = auth_info; + let create_req = req.into_inner(); + let schema_value = create_req.schema; + + if let Err(e) = + validate_jsonschema(&state.meta_schema, &schema_value) + { + return HttpResponse::BadRequest().body(e); + }; + + + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&schema_value); + + if let Err(e) = schema_compile_result { + return HttpResponse::BadRequest().body(String::from(format!("Bad schema: {:?}", e ))); + }; + let new_dimension = Dimension { - dimension: req.dimension.clone(), - priority: i32::from(req.priority), - type_: req.r#type, + dimension: create_req.dimension, + priority: i32::from(create_req.priority), + schema: schema_value, created_by: email, created_at: Utc::now(), }; diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs index 336210289..9b699498c 100644 --- a/crates/context-aware-config/src/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -1,10 +1,9 @@ use serde::Deserialize; - -use crate::db::models::DimensionType; +use serde_json::Value; #[derive(Deserialize)] pub struct CreateReq { pub dimension: String, pub priority: u16, - pub r#type: DimensionType, + pub schema: Value, } diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index e325dd2a9..ebcab9055 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -1,7 +1,7 @@ use crate::db::schema::cac_v1::{contexts, default_configs, dimensions, event_log}; use chrono::{offset::Utc, DateTime, NaiveDateTime}; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::Value; #[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Serialize, Debug)] @@ -18,27 +18,15 @@ pub struct Context { pub override_: Value, } -#[derive(Debug, Clone, Copy, diesel_derive_enum::DbEnum, Deserialize, Serialize)] -#[ExistingTypePath = "crate::db::schema::cac_v1::sql_types::DimensionType"] -#[DbValueStyle = "UPPERCASE"] -#[ExistingTypePath = "crate::db::schema::sql_types::DimensionType"] -pub enum DimensionType { - BOOL, - NUMBER, - STRING, - ARRAY, - OBJECT, -} - #[derive(Queryable, Selectable, Insertable, AsChangeset)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(dimension))] pub struct Dimension { pub dimension: String, pub priority: i32, - pub type_: DimensionType, pub created_at: DateTime, pub created_by: String, + pub schema: Value, } #[derive(Queryable, Selectable, Insertable, AsChangeset)] diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index f81be41e0..565173117 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -1,12 +1,6 @@ // @generated automatically by Diesel CLI. pub mod cac_v1 { - pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] - pub struct DimensionType; - } - diesel::table! { cac_v1.contexts (id) { id -> Varchar, @@ -31,16 +25,12 @@ pub mod cac_v1 { } diesel::table! { - use diesel::sql_types::*; - use super::sql_types::DimensionType; - cac_v1.dimensions (dimension) { dimension -> Varchar, priority -> Int4, - #[sql_name = "type"] - type_ -> DimensionType, created_at -> Timestamptz, created_by -> Varchar, + schema -> Json, } } diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index 0e2591253..ae3c2baaf 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -1,6 +1,6 @@ use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; -use jsonschema::{Draft, JSONSchema}; -use serde_json::json; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Value}; use std::collections::HashMap; pub fn get_default_config_validation_schema() -> JSONSchema { @@ -59,3 +59,67 @@ pub fn parse_headermap_safe(headermap: &HeaderMap) -> HashMap { headermap.iter().for_each(record_header); req_headers } + +pub fn get_meta_schema() -> JSONSchema { + let my_schema = json!({ + "type": "object", + "properties": { + "type": { + "enum": ["boolean", "number", "string"] + }, + }, + "required": ["type"], + + // # Add extra validation if needed for other primitive data types + "if": { + "properties": { "type": { "const": "string" } } + } + , "then": { + "oneOf": [ + { + "required": ["pattern"], + "properties": { "pattern": { "type": "string" } } + }, + { + "required": ["enum"], + "properties": { + "enum": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 1 + }, + } + } + ] + } + }); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&my_schema) + .expect("Something weird happened, failed to compile the schema for the Dimension schema!") +} + +/* + This step is required because an empty object + is also a valid JSON schema. So added required + validations for the input. +*/ +// TODO: Recursive validation. +pub fn validate_jsonschema( + validation_schema: &JSONSchema, + schema: &Value, +) -> Result<(), String> { + let res = match validation_schema.validate(schema) { + Ok(_) => Ok(()), + Err(e) => { + //TODO: Try & render as json. + let verrors = e.collect::>(); + Err(String::from(format!( + "Bad schema: {:?}", + verrors.as_slice() + ))) + } + }; + res +} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index dc429ed57..07da28f4d 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -20,7 +20,7 @@ use service_utils::{ }; use api::*; -use helpers::get_default_config_validation_schema; +use helpers::{get_default_config_validation_schema, get_meta_schema}; use experimentation_platform::api::*; @@ -86,6 +86,7 @@ async fn main() -> Result<()> { string_to_int(&deployment_id), string_to_int(&pod_identifier), )), + meta_schema: get_meta_schema(), })) .wrap( actix_web::middleware::DefaultHeaders::new() diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 4244a0ff1..4052203e8 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -23,6 +23,7 @@ pub struct AppState { pub admin_token: String, pub db_pool: Pool>, pub default_config_validation_schema: JSONSchema, + pub meta_schema: JSONSchema, pub experimentation_flags: ExperimentationFlags, pub snowflake_generator: Mutex, } diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json index c34753042..9274a3eb1 100644 --- a/postman/cac.postman_collection.json +++ b/postman/cac.postman_collection.json @@ -175,7 +175,7 @@ "language": "json" } }, - "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"type\":\"STRING\"}" + "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"schema\":{\"type\":\"string\",\"pattern\":\"^[a-z0-9].*$\"}}" }, "url": { "raw": "{{host}}/dimension", diff --git a/postman/cac/Dimension/Create Dimension/request.json b/postman/cac/Dimension/Create Dimension/request.json index 69f459bbc..7f2a9c941 100644 --- a/postman/cac/Dimension/Create Dimension/request.json +++ b/postman/cac/Dimension/Create Dimension/request.json @@ -22,7 +22,10 @@ "raw_json_formatted": { "dimension": "clientId", "priority": 100, - "type": "STRING" + "schema": { + "type": "string", + "pattern": "^[a-z0-9].*$" + } } }, "url": { diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json index 68dbfb73d..ffd8a3a8f 100644 --- a/postman/experimentation-platform.postman_collection.json +++ b/postman/experimentation-platform.postman_collection.json @@ -281,9 +281,9 @@ "", "function create_dimensions() {", " const dimensions = [", - " {name: \"os\", priority: 10, type: \"STRING\"},", - " {name: \"client\", priority: 100, type: \"STRING\"},", - " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", + " {name: \"os\", priority: 10, schema: { type: \"string\", enum: [\"android\", \"ios\", \"web\"] }},", + " {name: \"client\", priority: 100, schema: { type: \"string\", pattern: \".*\" }},", + " {name: \"variantIds\", priority: 1000, schema: { type: \"string\", pattern: \".*\" }}", " ];", "", " for (const dimension of dimensions) {", @@ -299,7 +299,7 @@ " \"raw\": JSON.stringify({", " \"dimension\": dimension.name,", " \"priority\": dimension.priority,", - " \"type\": dimension.type", + " \"schema\": dimension.schema", " })", " }", " };", diff --git a/postman/experimentation-platform/Create Experiment/event.prerequest.js b/postman/experimentation-platform/Create Experiment/event.prerequest.js index 6ffe321ac..633348069 100644 --- a/postman/experimentation-platform/Create Experiment/event.prerequest.js +++ b/postman/experimentation-platform/Create Experiment/event.prerequest.js @@ -40,9 +40,9 @@ function create_default_config_keys() { function create_dimensions() { const dimensions = [ - {name: "os", priority: 10, type: "STRING"}, - {name: "client", priority: 100, type: "STRING"}, - {name: "variantIds", priority: 1000, type: "STRING"} + {name: "os", priority: 10, schema: { type: "string", enum: ["android", "ios", "web"] }}, + {name: "client", priority: 100, schema: { type: "string", pattern: ".*" }}, + {name: "variantIds", priority: 1000, schema: { type: "string", pattern: ".*" }} ]; for (const dimension of dimensions) { @@ -58,7 +58,7 @@ function create_dimensions() { "raw": JSON.stringify({ "dimension": dimension.name, "priority": dimension.priority, - "type": dimension.type + "schema": dimension.schema }) } }; From dd1774615dcc98d9cde81e443ebade995f952a8c Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 7 Sep 2023 20:26:45 +0530 Subject: [PATCH 121/352] fix: fixed random timeouts in internal http calls to CAC --- Cargo.lock | 2 +- crates/experimentation-platform/Cargo.toml | 2 +- .../src/api/experiments/handlers.rs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4bb726f8..e6ed21709 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,7 +1142,7 @@ dependencies = [ "dotenv", "env_logger", "log", - "reqwest 0.9.24", + "reqwest 0.11.18", "rs-snowflake", "serde", "serde_json", diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 68a358c2a..88f58a9be 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -31,4 +31,4 @@ chrono = { version = "0.4", features = ["serde"] } diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } -reqwest = {version = "0.9.11"} +reqwest = {version = "0.11.18"} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 6d8ac1280..ca44bd7ee 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -52,7 +52,7 @@ async fn create( let DbConnection(mut conn) = db_conn; let override_keys = &req.override_keys; - + let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); @@ -131,8 +131,10 @@ async fn create( .bearer_auth(&state.admin_token) .json(&cac_operations) .send() + .await .map_err(|e| err::InternalServerErr(e.to_string()))? .json::>() + .await .map_err(|e| err::InternalServerErr(e.to_string()))?; // updating variants with context and override ids @@ -246,6 +248,7 @@ async fn conclude( .bearer_auth(&state.admin_token) .json(&operations) .send() + .await .map_err(|e| err::InternalServerErr(e.to_string()))?; if !response.status().is_success() { @@ -414,4 +417,4 @@ async fn ramp( "Traffic percentage has been updated for the experiment id : {}", exp_id ))); -} +} \ No newline at end of file From c249efc4a15482bbad3efcafa7fe8352c24735e2 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 8 Sep 2023 12:44:02 +0530 Subject: [PATCH 122/352] fix: failed build due to untracked schema.rs file changes --- crates/experimentation-platform/src/db/schema.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index e9b42d067..187ace544 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -2,10 +2,6 @@ pub mod cac_v1 { pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "dimension_type", schema = "cac_v1"))] - pub struct DimensionType; - #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] pub struct ExperimentStatusType; @@ -35,16 +31,12 @@ pub mod cac_v1 { } diesel::table! { - use diesel::sql_types::*; - use super::sql_types::DimensionType; - cac_v1.dimensions (dimension) { dimension -> Varchar, priority -> Int4, - #[sql_name = "type"] - type_ -> DimensionType, created_at -> Timestamptz, created_by -> Varchar, + schema -> Json, } } From 7cda6061aa0d4e5265c38ee93ef0f174ea1db30f Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 8 Sep 2023 12:58:15 +0530 Subject: [PATCH 123/352] ci: added 20 minutes timeout on pipeline --- Jenkinsfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 327b136c3..daa5b2dd1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,6 +4,9 @@ def getRegistryHost(aws_acc_id, region) { pipeline { agent { label 'hypersdk' } + options { + timeout(time: 20, unit: 'MINUTES') + } environment { REGION = "ap-south-1"; REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); From b63c7535b075cd38e1e8c06b66d3119a9bc8e57c Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Thu, 7 Sep 2023 13:03:36 +0530 Subject: [PATCH 124/352] feat: Adding generic eval --- Cargo.lock | 1 + crates/cac_client/Cargo.toml | 1 + crates/cac_client/src/eval.rs | 127 ++---------------- crates/cac_client/src/lib.rs | 5 +- crates/cac_client/src/utils/deep_merge.rs | 87 ------------ crates/cac_client/src/utils/mod.rs | 1 - .../src/api/config/handlers.rs | 4 +- 7 files changed, 17 insertions(+), 209 deletions(-) delete mode 100644 crates/cac_client/src/utils/deep_merge.rs diff --git a/Cargo.lock b/Cargo.lock index e6ed21709..fb6e61a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,7 @@ version = "0.1.0" dependencies = [ "actix-web", "chrono", + "json-patch", "jsonlogic", "log", "reqwest 0.11.18", diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 685354f2e..b7f400a80 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -13,3 +13,4 @@ reqwest = { version = "0.11.18", features = ["json"]} serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" log = "^0.4" +json-patch = "1.0.0" diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index 43f883cdb..65569871e 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -1,103 +1,16 @@ //NOTE this code is copied over from sdk-config-server with small changes for compatiblity //TODO refactor, make eval MJOS agnostic -use std::collections::HashMap; - -use crate::{utils::deep_merge::{deep_merge, patch_json}, Context}; +use crate::{Context}; use jsonlogic; -use serde::{de, Deserialize, Serialize}; -use serde_json::{from_value, json, to_value, Map, Value}; - -#[derive(Deserialize, Serialize)] -pub struct PathParams { - merchant_id: String, - platform: String, - environment: String, -} +use serde_json::{json, Map, Value}; -#[derive(Clone, Debug, Deserialize)] -struct MJOSOverride { - path: Vec, - value: Value, - etag: Option, -} - -fn create_nested_json(path_array: Vec, value: Value) -> serde_json::Result { - path_array.iter().rev().try_fold(value, |acc, key_string| { - to_value(HashMap::::from([( - key_string.to_string(), - acc, - )])) - }) -} - -type EtagMap = HashMap; -pub type Patch = (Value, EtagMap); - -fn form_override_object(input_config: Value) -> serde_json::Result { - let input_map: Map = from_value(input_config)?; - let patch_entries = input_map.get("patch_entries"); - let keys_to_be_overridden: Vec = - patch_entries.map_or(Ok(Vec::new()), |x| from_value(x.to_owned()))?; - let mut etags: EtagMap = HashMap::new(); - let patch_obj = keys_to_be_overridden - .iter() - .try_fold(json!({}), |acc, mapped_key| { - if let Some(MJOSOverride { - path: path_array, - value, - etag: mb_etag, - }) = input_map - .get(mapped_key) - .and_then(|x| -> Option { from_value(x.to_owned()).ok() }) - { - if let (Some(etag), Value::String(url)) = (mb_etag, value.clone()) { - etags.insert(url, etag); - } - let split_path = &path_array - .split_first() - .map(|(first, tail)| (first.as_str(), tail)); - let entries = match split_path { - Some(("live", tail)) => vec![ - path_array.clone(), - [vec!["new".to_string()], tail.to_vec()].concat(), - ], - Some(("new", tail)) => vec![ - path_array.clone(), - [vec!["new".to_string()], tail.to_vec()].concat(), - ], - _ => vec![path_array], - }; - - return entries.iter().try_fold(acc, |accumulator, val| { - deep_merge( - &accumulator, - &create_nested_json(val.to_owned(), json!(value))?, - ) - }); - } - Ok(acc) - }); - patch_obj.map(|x| (x, etags)) -} - -fn choose_merge_strategy( - allow_new_paths: bool, -) -> for<'a, 'b> fn(&'a Value, &'b Value) -> serde_json::Result { - if allow_new_paths { - deep_merge - } else { - patch_json - } -} fn get_overrides( query_data: &Value, - contexts: Vec, - overrides: Map, - allow_new_paths: bool, + contexts: &Vec, + overrides: &Map, ) -> serde_json::Result { - let merge_fn = choose_merge_strategy(allow_new_paths); let mut required_overrides: Value = json!({}); for context in contexts.iter() { @@ -105,7 +18,7 @@ fn get_overrides( if let Ok(Value::Bool(true)) = jsonlogic::apply(&context.condition, query_data) { for override_key in &context.override_with_keys { if let Some(overriden_value) = overrides.get(override_key) { - required_overrides = merge_fn(&required_overrides, overriden_value)?; + json_patch::merge(&mut required_overrides, overriden_value) } } } @@ -115,30 +28,12 @@ fn get_overrides( } pub fn eval_cac( - default_config: Value, - contexts: Vec, - overrides: Map, + mut default_config: Value, + contexts: &Vec, + overrides: &Map, query_data: &Value, ) -> serde_json::Result { - // get_overrides runs json logic and gives patch_entries from overrides.json - let overrides = get_overrides(query_data, contexts, overrides, true)?; - - // patch_entries received from overides after json logic are then merged over default_config - deep_merge(&default_config, &overrides) -} - -pub fn get_mjos_override( - query_data: Value, - contexts: Vec, - overrides: Map, - default_config: Value, -) -> serde_json::Result { - - let overrides_config = eval_cac(default_config, contexts, overrides, &query_data)?; - - if Some(&json!([])) == overrides_config.get("patch_entries") { - return Err(de::Error::custom("No patches found")); - } - // overriding object is then generated from default_config - form_override_object(overrides_config) + let overrides = get_overrides(&query_data, &contexts, &overrides)?; + json_patch::merge(&mut default_config, &overrides); + Ok(default_config) } diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 01edbf2cb..5c07a217f 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -8,7 +8,6 @@ use actix_web::{ }, web::Data, }; -use eval::{get_mjos_override, Patch}; use std::{convert::identity, sync::RwLock, time::Duration}; use utils::core::MapError; use chrono::{DateTime, Utc}; @@ -115,9 +114,9 @@ impl Client { .map_err_to_string() } - pub fn eval(&self, query_data: Value) -> Result { + pub fn eval(&self, query_data: Value) -> Result { let cac = self.config.read().map_err_to_string()?; - get_mjos_override(query_data, cac.contexts.clone(), cac.overrides.clone(), json!(cac.default_configs)).map_err_to_string() + eval::eval_cac( json!(cac.default_configs), &cac.contexts, &cac.overrides, &query_data).map_err_to_string() } } diff --git a/crates/cac_client/src/utils/deep_merge.rs b/crates/cac_client/src/utils/deep_merge.rs deleted file mode 100644 index ec6feaa51..000000000 --- a/crates/cac_client/src/utils/deep_merge.rs +++ /dev/null @@ -1,87 +0,0 @@ -//NOTE this code is copied over from sdk-config-server - -use std::collections::HashMap; - -use serde_json::{from_value, to_value, Error, Value}; - -fn type_of(a: &Value) -> String { - if a.is_boolean() { - return "Boolean".to_string(); - } - - if a.is_number() { - return "Number".to_string(); - } - - if a.is_string() { - return "String".to_string(); - } - - if a.is_array() { - return "Array".to_string(); - } - - if a.is_null() { - return "Null".to_string(); - } - - "Object".to_string() -} - -fn merge_json_helper( - tree1: &Value, - tree2: &Value, - add_new_paths: bool, -) -> Result, Error> { - let mut result_hash_map: HashMap = from_value(tree1.to_owned())?; - - if let Some(subtree) = tree2.as_object() { - for (key, overriden_value) in subtree.iter() { - if let Some(default_value) = tree1.get(key) { - if type_of(default_value) == "Object" { - let merged_sub_tree = - merge_json_helper(default_value, overriden_value, add_new_paths)?; - result_hash_map.insert(key.to_string(), to_value(merged_sub_tree)?); - } else { - result_hash_map.insert(key.to_string(), overriden_value.clone()); - } - } else if add_new_paths { - result_hash_map.insert(key.to_string(), overriden_value.clone()); - } - } - } - - Ok(result_hash_map) -} - -pub fn patch_json(tree1: &Value, tree2: &Value) -> Result { - to_value(merge_json_helper(tree1, tree2, false)?) -} - -pub fn deep_merge(tree1: &Value, tree2: &Value) -> Result { - to_value(merge_json_helper(tree1, tree2, true)?) -} - -// ************ Tests ************* - -#[cfg(test)] -mod tests { - use serde_json::json; - - #[test] - fn patch_json() { - let a = json!({"x":"y"}); - let b = json!({"x":"z", "p":"q"}); - assert_eq!(super::patch_json(&a, &b).unwrap(), json!({"x":"z"})); - } - - #[test] - fn deep_merge() { - let a = json!({"x":"y"}); - let b = json!({"x":"z", "p":"q"}); - assert_eq!( - super::deep_merge(&a, &b).unwrap(), - json!({"x":"z", "p": "q"}) - ); - } -} diff --git a/crates/cac_client/src/utils/mod.rs b/crates/cac_client/src/utils/mod.rs index beafec241..5a7ca06a4 100644 --- a/crates/cac_client/src/utils/mod.rs +++ b/crates/cac_client/src/utils/mod.rs @@ -1,2 +1 @@ -pub mod deep_merge; pub mod core; diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index bd7defb46..00f8a4c3d 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -138,8 +138,8 @@ async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_w .json( eval_cac( json!(res.default_configs), - cac_client_contexts, - res.overrides, + &cac_client_contexts, + &res.overrides, &json!(query_params_map), ).map_err_to_internal_server("cac eval failed", Null)? ) From 4637fd9ff40dc6a66a190822e776c7a70b38fd5e Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Wed, 23 Aug 2023 20:06:35 +0530 Subject: [PATCH 125/352] added audit_log_uuid in response header for get_config api --- Cargo.lock | 1 + crates/context-aware-config/Cargo.toml | 1 + crates/context-aware-config/src/main.rs | 9 +- .../src/middlewares/audit_response_header.rs | 112 ++++++++++++++++++ .../src/middlewares/mod.rs | 1 + 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 crates/context-aware-config/src/middlewares/audit_response_header.rs create mode 100644 crates/context-aware-config/src/middlewares/mod.rs diff --git a/Cargo.lock b/Cargo.lock index fb6e61a2c..351281438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,6 +703,7 @@ dependencies = [ "env_logger", "experimentation-platform", "futures 0.3.28", + "futures-util", "json-patch", "jsonschema", "log", diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index c64480943..43c025c93 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -52,3 +52,4 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"]} valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} futures = "0.3.28" actix-http = "3.3.1" +futures-util = "0.3.28" diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 07da28f4d..ef6030a3d 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -2,6 +2,7 @@ mod api; mod db; mod helpers; mod logger; +mod middlewares; use dotenv; use logger::{init_log_subscriber, CustomRootSpanBuilder}; @@ -19,6 +20,8 @@ use service_utils::{ service::types::{AppState, ExperimentationFlags}, }; +use crate::middlewares::audit_response_header::{AuditHeader, TableName}; + use api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; @@ -102,7 +105,11 @@ async fn main() -> Result<()> { .service(scope("/context").service(context::endpoints())) .service(scope("/dimension").service(dimension::endpoints())) .service(scope("/default-config").service(default_config::endpoints())) - .service(scope("/config").service(config::endpoints())) + .service( + scope("/config") + .wrap(AuditHeader::new(TableName::Contexts)) + .service(config::endpoints()), + ) .service(scope("/audit").service(audit_log::endpoints())) .service(experiments::endpoints()) }) diff --git a/crates/context-aware-config/src/middlewares/audit_response_header.rs b/crates/context-aware-config/src/middlewares/audit_response_header.rs new file mode 100644 index 000000000..0093d6096 --- /dev/null +++ b/crates/context-aware-config/src/middlewares/audit_response_header.rs @@ -0,0 +1,112 @@ +use std::future::{ready, Ready}; + +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::header::{HeaderName, HeaderValue}, + web::Data, + Error, +}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use futures_util::future::LocalBoxFuture; +use service_utils::service::types::AppState; + +use crate::db::schema::cac_v1::event_log::dsl as event_log; +use uuid::Uuid; + +#[derive(Clone, Copy, Debug, strum_macros::Display)] +#[strum(serialize_all = "snake_case")] +#[allow(dead_code)] +pub enum TableName { + Contexts, + DefaultConfigs, + Dimensions, + Experiments, +} + +pub struct AuditHeader { + table_name: TableName, +} + +impl AuditHeader { + pub fn new(table_name: TableName) -> Self { + AuditHeader { table_name } + } +} + +impl Transform for AuditHeader +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = AuditHeaderMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AuditHeaderMiddleware { + service, + table_name: self.table_name, + })) + } +} + +pub struct AuditHeaderMiddleware { + service: S, + table_name: TableName, +} + +impl Service for AuditHeaderMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let app_state = req.app_data::>(); + let mut db_conn_option = None; + + if let Some(app_data) = app_state { + match app_data.db_pool.get() { + Ok(conn) => db_conn_option = Some(conn), + Err(_) => log::error!("Failed to get connection"), + } + } else { + log::error!("App State not available"); + } + + let fut = self.service.call(req); + let table_name = self.table_name; + + Box::pin(async move { + let mut res = fut.await?; + + if let Some(mut conn) = db_conn_option { + let uuid = event_log::event_log + .select(event_log::id) + .filter(event_log::table_name.eq(table_name.to_string())) + .order_by(event_log::timestamp.desc()) + .first::(&mut conn); + + if let Ok(uuid) = uuid { + res.headers_mut().insert( + HeaderName::from_static("x-audit-id"), + HeaderValue::from_str(&uuid.to_string()) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), + ); + } else { + log::error!("Unable to fetch uuid"); + } + } + Ok(res) + }) + } +} diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/context-aware-config/src/middlewares/mod.rs new file mode 100644 index 000000000..2ee97781a --- /dev/null +++ b/crates/context-aware-config/src/middlewares/mod.rs @@ -0,0 +1 @@ +pub mod audit_response_header; From a2fde0680f32983b32d182c99906dd53737e88ef Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Mon, 11 Sep 2023 18:56:51 +0530 Subject: [PATCH 126/352] feat: cac eval return update --- crates/cac_client/src/eval.rs | 6 +++--- crates/cac_client/src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index 65569871e..b45bc11da 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -32,8 +32,8 @@ pub fn eval_cac( contexts: &Vec, overrides: &Map, query_data: &Value, -) -> serde_json::Result { +) -> serde_json::Result> { let overrides = get_overrides(&query_data, &contexts, &overrides)?; json_patch::merge(&mut default_config, &overrides); - Ok(default_config) -} + serde_json::from_value(default_config) +} \ No newline at end of file diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 5c07a217f..ac26e4c7f 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -114,7 +114,7 @@ impl Client { .map_err_to_string() } - pub fn eval(&self, query_data: Value) -> Result { + pub fn eval(&self, query_data: Value) -> Result, String> { let cac = self.config.read().map_err_to_string()?; eval::eval_cac( json!(cac.default_configs), &cac.contexts, &cac.overrides, &query_data).map_err_to_string() } From a9c5b049544927836a7b0dcdd8fc473ff913168c Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Tue, 12 Sep 2023 13:56:55 +0530 Subject: [PATCH 127/352] fix: eval param fix --- crates/cac_client/src/eval.rs | 6 +++--- crates/cac_client/src/lib.rs | 2 +- crates/context-aware-config/src/api/config/handlers.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index b45bc11da..db20f65c6 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -7,7 +7,7 @@ use serde_json::{json, Map, Value}; fn get_overrides( - query_data: &Value, + query_data: &Map, contexts: &Vec, overrides: &Map, ) -> serde_json::Result { @@ -15,7 +15,7 @@ fn get_overrides( for context in contexts.iter() { // TODO :: Add semantic version comparator in Lib - if let Ok(Value::Bool(true)) = jsonlogic::apply(&context.condition, query_data) { + if let Ok(Value::Bool(true)) = jsonlogic::apply(&context.condition, &json!(query_data)) { for override_key in &context.override_with_keys { if let Some(overriden_value) = overrides.get(override_key) { json_patch::merge(&mut required_overrides, overriden_value) @@ -31,7 +31,7 @@ pub fn eval_cac( mut default_config: Value, contexts: &Vec, overrides: &Map, - query_data: &Value, + query_data: &Map, ) -> serde_json::Result> { let overrides = get_overrides(&query_data, &contexts, &overrides)?; json_patch::merge(&mut default_config, &overrides); diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index ac26e4c7f..44c85a369 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -114,7 +114,7 @@ impl Client { .map_err_to_string() } - pub fn eval(&self, query_data: Value) -> Result, String> { + pub fn eval(&self, query_data: Map) -> Result, String> { let cac = self.config.read().map_err_to_string()?; eval::eval_cac( json!(cac.default_configs), &cac.contexts, &cac.overrides, &query_data).map_err_to_string() } diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 00f8a4c3d..3eae93b99 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -107,7 +107,7 @@ async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_w Query::>::from_query(req.query_string()) .map_err(|_| ErrorBadRequest("error getting query params"))?; - let mut query_params_map = Map::new(); + let mut query_params_map: serde_json::Map = Map::new(); for item in params.0.into_iter() { query_params_map.insert( @@ -140,7 +140,7 @@ async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_w json!(res.default_configs), &cac_client_contexts, &res.overrides, - &json!(query_params_map), + &query_params_map, ).map_err_to_internal_server("cac eval failed", Null)? ) ) From a5f8757e0945f78a53bb94db018a18cb7f3bc895 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 12 Sep 2023 10:37:02 +0000 Subject: [PATCH 128/352] chore(version): v0.6.0 [skip ci] --- CHANGELOG.md | 17 +++++++++++++++++ Cargo.lock | 8 ++++---- crates/cac_client/CHANGELOG.md | 9 +++++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 9 +++++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 7 +++++++ crates/experimentation-platform/Cargo.toml | 4 ++-- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 10 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b8b6f4b1..19028497e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.6.0 - 2023-09-12 +### Package updates +- cac_client bumped to cac_client-v0.2.0 +- service-utils bumped to service-utils-v0.4.0 +- context-aware-config bumped to context-aware-config-v0.5.0 +- experimentation-platform bumped to experimentation-platform-v0.3.1 +### Global changes +#### Bug Fixes +- fixed random timeouts in internal http calls to CAC - (a4e95a3) - Shubhranshu Sanjeev +#### Continuous Integration +- added 20 minutes timeout on pipeline - (2f6bf9e) - Shubhranshu Sanjeev +#### Features +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + ## v0.5.1 - 2023-09-06 ### Package updates - superposition_client bumped to superposition_client-v0.1.2 diff --git a/Cargo.lock b/Cargo.lock index 351281438..4445a41dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,7 +562,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix-web", "chrono", @@ -686,7 +686,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix", "actix-http", @@ -1133,7 +1133,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.3.0" +version = "0.3.1" dependencies = [ "actix", "actix-web", @@ -3048,7 +3048,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix", "actix-web", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index b3c4a1b13..ac2588f8a 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.2.0 - 2023-09-12 +#### Bug Fixes +- PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra +#### Features +- PICAF-24223 cac eval return update - (d558ddc) - Pratik Mishra +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra + +- - - + ## cac_client-v0.1.0 - 2023-09-01 #### Features - [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index b7f400a80..69d787898 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 057da9285..03ef01004 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.5.0 - 2023-09-12 +#### Bug Fixes +- PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra +#### Features +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + ## context-aware-config-v0.4.0 - 2023-09-06 #### Features - [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 43c025c93..960647bbb 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.4.0" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 3477d51c7..0c298be57 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.3.1 - 2023-09-12 +#### Bug Fixes +- failed build due to untracked schema.rs file changes - (5bc4eae) - Shubhranshu Sanjeev +- fixed random timeouts in internal http calls to CAC - (a4e95a3) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.3.0 - 2023-09-06 #### Features - [PICAF-24160] record the chosen variant after conclude - (1c3c6e6) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 88f58a9be..29d8d3488 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.3.0" +version = "0.3.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -31,4 +31,4 @@ chrono = { version = "0.4", features = ["serde"] } diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } -reqwest = {version = "0.11.18"} \ No newline at end of file +reqwest = {version = "0.11.18"} diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 64b58630b..d2b45ca60 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.4.0 - 2023-09-12 +#### Features +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + ## service-utils-v0.3.0 - 2023-09-06 #### Features - [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index bd5b40825..abc087997 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8e311c7d9a4d8d5e77e1203803ba4aad3cf01fdf Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 18 Sep 2023 16:16:12 +0530 Subject: [PATCH 129/352] fix: patching overrides on default-config instead of merge --- crates/cac_client/src/eval.rs | 33 ++++++-- crates/cac_client/src/lib.rs | 35 ++++---- crates/cac_client/src/utils/core.rs | 1 - .../src/api/config/handlers.rs | 84 +++++++++---------- 4 files changed, 83 insertions(+), 70 deletions(-) diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index db20f65c6..512bbe46c 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -1,11 +1,10 @@ //NOTE this code is copied over from sdk-config-server with small changes for compatiblity //TODO refactor, make eval MJOS agnostic -use crate::{Context}; +use crate::{utils::core::MapError, Context}; use jsonlogic; use serde_json::{json, Map, Value}; - fn get_overrides( query_data: &Map, contexts: &Vec, @@ -15,7 +14,9 @@ fn get_overrides( for context in contexts.iter() { // TODO :: Add semantic version comparator in Lib - if let Ok(Value::Bool(true)) = jsonlogic::apply(&context.condition, &json!(query_data)) { + if let Ok(Value::Bool(true)) = + jsonlogic::apply(&context.condition, &json!(query_data)) + { for override_key in &context.override_with_keys { if let Some(overriden_value) = overrides.get(override_key) { json_patch::merge(&mut required_overrides, overriden_value) @@ -27,13 +28,29 @@ fn get_overrides( Ok(required_overrides) } +fn merge_overrides_on_default_config( + default_config: &mut Map, + overrides: Map, +) { + overrides.into_iter().for_each(|(key, val)| { + if let Some(og_val) = default_config.get_mut(&key) { + json_patch::merge(og_val, &val) + } else { + log::error!("CAC: found non-default_config key: {key} in overrides"); + } + }) +} + pub fn eval_cac( - mut default_config: Value, + mut default_config: Map, contexts: &Vec, overrides: &Map, query_data: &Map, -) -> serde_json::Result> { - let overrides = get_overrides(&query_data, &contexts, &overrides)?; - json_patch::merge(&mut default_config, &overrides); - serde_json::from_value(default_config) +) -> Result, String> { + let overrides: Map = get_overrides(&query_data, &contexts, &overrides) + .and_then(|x| serde_json::from_value(x)) + .map_err_to_string()?; + merge_overrides_on_default_config(&mut default_config, overrides); + let overriden_config = default_config; + Ok(overriden_config) } \ No newline at end of file diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 44c85a369..76f321650 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -1,19 +1,16 @@ -mod utils; mod eval; +mod utils; use actix_web::{ - rt::{ - self, - time::interval, - }, + rt::{self, time::interval}, web::Data, }; -use std::{convert::identity, sync::RwLock, time::Duration}; -use utils::core::MapError; use chrono::{DateTime, Utc}; use reqwest::{RequestBuilder, StatusCode}; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value, json}; +use serde_json::{Map, Value}; +use std::{convert::identity, sync::RwLock, time::Duration}; +use utils::core::MapError; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { @@ -94,10 +91,7 @@ impl Client { let mut interval = interval(self.polling_interval); loop { interval.tick().await; - let result = self - .update_cac() - .await - .unwrap_or_else(identity); + let result = self.update_cac().await.unwrap_or_else(identity); log::info!("{result}",); } }); @@ -108,15 +102,20 @@ impl Client { } pub fn get_last_modified(&'static self) -> Result, String> { - self.last_modified - .read() - .map(|t| *t) - .map_err_to_string() + self.last_modified.read().map(|t| *t).map_err_to_string() } - pub fn eval(&self, query_data: Map) -> Result, String> { + pub fn eval( + &self, + query_data: Map, + ) -> Result, String> { let cac = self.config.read().map_err_to_string()?; - eval::eval_cac( json!(cac.default_configs), &cac.contexts, &cac.overrides, &query_data).map_err_to_string() + eval::eval_cac( + cac.default_configs.to_owned(), + &cac.contexts, + &cac.overrides, + &query_data, + ) } } diff --git a/crates/cac_client/src/utils/core.rs b/crates/cac_client/src/utils/core.rs index 9437d5d14..e00789daf 100644 --- a/crates/cac_client/src/utils/core.rs +++ b/crates/cac_client/src/utils/core.rs @@ -12,4 +12,3 @@ where self.map_err(|e| e.to_string()) } } - diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 3eae93b99..187a02a46 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -4,21 +4,27 @@ use super::types::Config; use crate::db::schema::cac_v1::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; -use actix_web::{error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope}; +use actix_web::{ + error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope, +}; +use cac_client::eval_cac; use chrono::{DateTime, NaiveDateTime, Utc}; -use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl, r2d2::{PooledConnection, ConnectionManager}, PgConnection}; +use diesel::{ + dsl::max, + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; use serde_json::{json, Map, Value, Value::Null}; use service_utils::{helpers::ToActixErr, service::types::DbConnection}; -use cac_client::eval_cac; pub fn endpoints() -> Scope { - Scope::new("") - .service(get) - .service(get_resolved_config) + Scope::new("").service(get).service(get_resolved_config) } - -fn is_not_modified (req: &HttpRequest, conn: &mut PooledConnection>) -> actix_web::Result { +fn is_not_modified( + req: &HttpRequest, + conn: &mut PooledConnection>, +) -> actix_web::Result { let max_created_at: Option = event_log::event_log .select(max(event_log::timestamp)) .filter(event_log::table_name.eq("contexts")) @@ -38,8 +44,9 @@ fn is_not_modified (req: &HttpRequest, conn: &mut PooledConnection>) -> actix_web::Result { - +async fn generate_cac( + conn: &mut PooledConnection>, +) -> actix_web::Result { let contexts_vec = ctxt::contexts .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) .order_by((ctxt::priority.asc(), ctxt::created_at.asc())) @@ -78,33 +85,26 @@ async fn generate_cac(conn: &mut PooledConnection actix_web::Result { - let DbConnection(mut conn) = db_conn; if is_not_modified(&req, &mut conn)? { return Ok(HttpResponse::NotModified().finish()); } - Ok( - HttpResponse::Ok() - .json( - generate_cac(&mut conn) - .await? - ) - ) + Ok(HttpResponse::Ok().json(generate_cac(&mut conn).await?)) } #[get("/resolve")] -async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result { - +async fn get_resolved_config( + req: HttpRequest, + db_conn: DbConnection, +) -> actix_web::Result { let DbConnection(mut conn) = db_conn; - let params = - Query::>::from_query(req.query_string()) + let params = Query::>::from_query(req.query_string()) .map_err(|_| ErrorBadRequest("error getting query params"))?; let mut query_params_map: serde_json::Map = Map::new(); @@ -113,11 +113,8 @@ async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_w query_params_map.insert( item.0, item.1 - .parse::() - .map_or_else( - |_| json!(item.1), - |int_val| json!(int_val) - ) + .parse::() + .map_or_else(|_| json!(item.1), |int_val| json!(int_val)), ); } @@ -127,21 +124,22 @@ async fn get_resolved_config(req: HttpRequest, db_conn: DbConnection) -> actix_w let res = generate_cac(&mut conn).await?; - let cac_client_contexts = - res.contexts.into_iter().map(|val| cac_client::Context { + let cac_client_contexts = res + .contexts + .into_iter() + .map(|val| cac_client::Context { condition: val.condition, - override_with_keys: val.override_with_keys - }).collect(); - - - Ok(HttpResponse::Ok() - .json( - eval_cac( - json!(res.default_configs), - &cac_client_contexts, - &res.overrides, - &query_params_map, - ).map_err_to_internal_server("cac eval failed", Null)? + override_with_keys: val.override_with_keys, + }) + .collect(); + + Ok(HttpResponse::Ok().json( + eval_cac( + res.default_configs, + &cac_client_contexts, + &res.overrides, + &query_params_map, ) - ) + .map_err_to_internal_server("cac eval failed", Null)?, + )) } From 5c471b249354b097e956492a4cc5954d6aa1ec19 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 20 Sep 2023 13:10:56 +0000 Subject: [PATCH 130/352] chore(version): v0.6.1 [skip ci] --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 4 ++-- crates/cac_client/CHANGELOG.md | 6 ++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19028497e..d9afa03f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.6.1 - 2023-09-20 +### Package updates +- cac_client bumped to cac_client-v0.2.1 +- context-aware-config bumped to context-aware-config-v0.5.1 +### Global changes + +- - - + ## v0.6.0 - 2023-09-12 ### Package updates - cac_client bumped to cac_client-v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index 4445a41dc..3ba77c67c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,7 +562,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.2.0" +version = "0.2.1" dependencies = [ "actix-web", "chrono", @@ -686,7 +686,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.5.0" +version = "0.5.1" dependencies = [ "actix", "actix-http", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index ac2588f8a..8d545fd0b 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.2.1 - 2023-09-20 +#### Bug Fixes +- PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan + +- - - + ## cac_client-v0.2.0 - 2023-09-12 #### Bug Fixes - PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 69d787898..0fe1de4f8 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.2.0" +version = "0.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 03ef01004..69babcfb0 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.5.1 - 2023-09-20 +#### Bug Fixes +- PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan + +- - - + ## context-aware-config-v0.5.0 - 2023-09-12 #### Bug Fixes - PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 960647bbb..b8b2ab304 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.5.0" +version = "0.5.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From f14f9dee9d76a2daad62a049bc100866e802eae1 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Mon, 4 Sep 2023 23:06:21 +0530 Subject: [PATCH 131/352] Stabilize & Revert API --- Cargo.lock | 17 +++++ crates/context-aware-config/Cargo.toml | 1 + crates/context-aware-config/src/main.rs | 2 +- .../src/api/experiments/handlers.rs | 47 +++++++++----- .../src/api/experiments/types.rs | 4 +- crates/external/Cargo.toml | 24 +++++++ .../external/src/api/external_api/handlers.rs | 65 +++++++++++++++++++ .../external/src/api/external_api/helpers.rs | 28 ++++++++ crates/external/src/api/external_api/mod.rs | 3 + crates/external/src/api/mod.rs | 1 + crates/external/src/lib.rs | 2 + 11 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 crates/external/Cargo.toml create mode 100644 crates/external/src/api/external_api/handlers.rs create mode 100644 crates/external/src/api/external_api/helpers.rs create mode 100644 crates/external/src/api/external_api/mod.rs create mode 100644 crates/external/src/api/mod.rs create mode 100644 crates/external/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3ba77c67c..d58f3a5ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,6 +702,7 @@ dependencies = [ "dotenv", "env_logger", "experimentation-platform", + "external", "futures 0.3.28", "futures-util", "json-patch", @@ -1154,6 +1155,22 @@ dependencies = [ "uuid 0.8.2", ] +[[package]] +name = "external" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "dotenv", + "experimentation-platform", + "log", + "reqwest 0.9.24", + "serde", + "serde_json", + "service-utils", +] + [[package]] name = "failure" version = "0.1.8" diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index b8b2ab304..d9fa55ea7 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -53,3 +53,4 @@ valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} futures = "0.3.28" actix-http = "3.3.1" futures-util = "0.3.28" +external = { path = "../external"} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index ef6030a3d..50f062f9d 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -111,7 +111,7 @@ async fn main() -> Result<()> { .service(config::endpoints()), ) .service(scope("/audit").service(audit_log::endpoints())) - .service(experiments::endpoints()) + .service(external::endpoints(experiments::endpoints(scope("/experiments")))) }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index ca44bd7ee..85125585d 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -6,7 +6,7 @@ use actix_web::{ HttpRequest, HttpResponse, Scope, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, r2d2::{PooledConnection, ConnectionManager}, PgConnection}; use service_utils::{ errors::types::{Error as err, ErrorResponse}, @@ -31,12 +31,12 @@ use crate::{ db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, }; -pub fn endpoints() -> Scope { - Scope::new("/experiments") +pub fn endpoints(scope : Scope) -> Scope { + scope .service(create) - .service(conclude) + .service(conclude_handler) .service(list_experiments) - .service(get_experiment) + .service(get_experiment_handler) .service(ramp) } @@ -173,19 +173,29 @@ async fn create( } #[patch("/{experiment_id}/conclude")] -async fn conclude( +async fn conclude_handler( state: Data, path: web::Path, req: web::Json, db_conn: DbConnection, auth_info: AuthenticationInfo, ) -> app::Result> { + let DbConnection(conn) = db_conn; + let response = conclude(state, path.into_inner(), req.into_inner(), conn, auth_info).await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +pub async fn conclude( + state: Data, + experiment_id: i64, + req: ConcludeExperimentRequest, + mut conn: PooledConnection>, + auth_info: AuthenticationInfo, +) -> app::Result { use crate::db::schema::cac_v1::experiments::dsl; - let experiment_id: i64 = path.into_inner(); - let winner_variant_id: String = req.into_inner().chosen_variant.to_owned(); + let winner_variant_id: String = req.chosen_variant.to_owned(); - let DbConnection(mut conn) = db_conn; let experiment: Experiment = dsl::experiments .find(experiment_id) .get_result::(&mut conn)?; @@ -271,7 +281,7 @@ async fn conclude( )) .get_result::(&mut conn)?; - return Ok(Json(ExperimentResponse::from(updated_experiment))); + return Ok(updated_experiment); } #[get("")] @@ -341,20 +351,25 @@ async fn list_experiments( } #[get("/{id}")] -async fn get_experiment( +async fn get_experiment_handler( params: web::Path, db_conn: DbConnection, ) -> app::Result> { - use crate::db::schema::cac_v1::experiments::dsl::*; - - let experiment_id = params.into_inner(); let DbConnection(mut conn) = db_conn; + let response = get_experiment(params.into_inner(), &mut conn)?; + return Ok(Json(ExperimentResponse::from(response))); +} +pub fn get_experiment( + experiment_id: i64, + conn: &mut PooledConnection>, +) -> app::Result { + use crate::db::schema::cac_v1::experiments::dsl::*; let result: Experiment = experiments .find(experiment_id) - .get_result::(&mut conn)?; + .get_result::(conn)?; - return Ok(Json(ExperimentResponse::from(result))); + return Ok(result); } #[patch("/{id}/ramp")] diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index b6992e97d..4f4e6f40d 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -5,7 +5,7 @@ use service_utils::helpers::deserialize_stringified_list; use crate::db::models::{self, ExperimentStatusType}; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)] pub enum VariantType { CONTROL, EXPERIMENTAL, @@ -48,7 +48,7 @@ impl From for ExperimentCreateResponse { // Same as models::Experiments but `id` field is String // JS have limitation of 53-bit integers, so on // deserializing from JSON to JS Object will lead incorrect `id` values -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ExperimentResponse { pub id: String, pub created_at: DateTime, diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml new file mode 100644 index 000000000..16db5a103 --- /dev/null +++ b/crates/external/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "external" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = "0.15.0" +# Https server framework +actix = "0.13.0" +actix-web = "4.0.0" +# To serialize and deserialize objects from json +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +# For logging and debugging +log = "^0.4" +# date and time +chrono = { version = "0.4", features = ["serde"] } +# ORM +service-utils = { path = "../service-utils" } +reqwest = {version = "0.9.11"} +experimentation-platform = { path = "../experimentation-platform"} diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs new file mode 100644 index 000000000..abc2a6c81 --- /dev/null +++ b/crates/external/src/api/external_api/handlers.rs @@ -0,0 +1,65 @@ +use actix_web::{ + patch, + web::{self, Data, Json}, + Scope, +}; + +use experimentation_platform::{ + api::experiments::{ + handlers::{get_experiment, conclude}, + types::{ExperimentResponse, ConcludeExperimentRequest, VariantType} + }, + db::models::Experiment +}; +use crate::api::external_api::helpers::fetch_variant_id; + +use service_utils::{ + service::types::{AppState, AuthenticationInfo, DbConnection}, + types as app, +}; + +pub fn endpoints(scope: Scope) -> Scope { + scope + .service(stabilize) + .service(revert) +} + +#[patch("/{id}/stabilize")] +async fn stabilize( + params: web::Path, + state: Data, + db_conn: DbConnection, + auth_info: AuthenticationInfo, +) -> app::Result> { + let response = conclude_experiment(params.into_inner(), state, db_conn, auth_info, VariantType::EXPERIMENTAL).await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +#[patch("/{id}/revert")] +async fn revert( + params: web::Path, + state: Data, + db_conn: DbConnection, + auth_info: AuthenticationInfo, +) -> app::Result> { + let response = conclude_experiment(params.into_inner(), state, db_conn, auth_info, VariantType::CONTROL).await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +pub async fn conclude_experiment( + exp_id: i64, + state: Data, + db_conn: DbConnection, + auth_info: AuthenticationInfo, + variant: VariantType, +) -> app::Result { + let DbConnection(mut conn) = db_conn; + + let experiment = get_experiment(exp_id, &mut conn)?; + let id = fetch_variant_id(experiment, variant)?; + let req_body = ConcludeExperimentRequest { + chosen_variant : id.to_string() + }; + let response = conclude (state, exp_id, req_body, conn, auth_info).await?; + return Ok(response); +} diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs new file mode 100644 index 000000000..af93bda2c --- /dev/null +++ b/crates/external/src/api/external_api/helpers.rs @@ -0,0 +1,28 @@ +use experimentation_platform::{ + api::experiments::types::{VariantType, Variant}, + db::models::Experiment +}; + +use service_utils::{ + errors::types::Error as err, + types as app, +}; + +pub fn fetch_variant_id( + experiment: Experiment, + variant: VariantType, +) -> app::Result { + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|e| { + log::error!("parsing to variant type failed with err: {e}"); + err::InternalServerErr("".to_string()) + })?; + + for ele in experiment_variants { + if ele.variant_type == variant { + return Ok(ele.id); + } + } + log::info!("Failed to fetch variant {:?} id for exp {}", variant, experiment.id); + return Err(err::InternalServerErr("".to_string())); +} diff --git a/crates/external/src/api/external_api/mod.rs b/crates/external/src/api/external_api/mod.rs new file mode 100644 index 000000000..f98852c0f --- /dev/null +++ b/crates/external/src/api/external_api/mod.rs @@ -0,0 +1,3 @@ +pub mod handlers; +pub mod helpers; +pub use handlers::endpoints; \ No newline at end of file diff --git a/crates/external/src/api/mod.rs b/crates/external/src/api/mod.rs new file mode 100644 index 000000000..daacf5f5b --- /dev/null +++ b/crates/external/src/api/mod.rs @@ -0,0 +1 @@ +pub mod external_api; \ No newline at end of file diff --git a/crates/external/src/lib.rs b/crates/external/src/lib.rs new file mode 100644 index 000000000..bdbd202bd --- /dev/null +++ b/crates/external/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub use api::external_api::endpoints; \ No newline at end of file From df0adf00f10bebe6e0db96776fc7c96ebac95536 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan Date: Mon, 25 Sep 2023 15:22:41 +0530 Subject: [PATCH 132/352] ci(flake.nix): pin nodejs version to 18 in flake --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 6d9700182..54860de36 100644 --- a/flake.nix +++ b/flake.nix @@ -50,6 +50,7 @@ pkgconfig awscli jq + nodejs_18 ]; darwinPkgs = with pkgs; [ darwin.apple_sdk.frameworks.Security @@ -59,4 +60,4 @@ }; } ); -} +} \ No newline at end of file From 6a5fe1f0dfffbb405c02c8e32e4e28c6b2216311 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 3 Oct 2023 17:34:37 +0530 Subject: [PATCH 133/352] feat: cors middleware attached - whitelists juspay-portal origins --- .env.example | 1 + Cargo.lock | 99 +++++++++++-------- crates/context-aware-config/Cargo.toml | 1 + crates/context-aware-config/src/main.rs | 3 +- .../src/middlewares/mod.rs | 24 +++++ 5 files changed, 87 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 72300f92e..1adce9ffb 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,4 @@ ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" HOSTNAME="---" +MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d58f3a5ea..f23f9a702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "actix-cors" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec 1.10.0", +] + [[package]] name = "actix-http" version = "3.3.1" @@ -689,6 +704,7 @@ name = "context-aware-config" version = "0.5.1" dependencies = [ "actix", + "actix-cors", "actix-http", "actix-web", "base64 0.21.2", @@ -709,7 +725,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.9.24", + "reqwest 0.11.18", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -894,15 +910,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "cxx" version = "1.0.94" @@ -1626,19 +1633,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.17.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes 0.4.12", - "ct-logs", - "futures 0.1.31", - "hyper 0.12.36", + "futures-util", + "http 0.2.9", + "hyper 0.14.26", "rustls", - "tokio-io", + "tokio 1.29.1", "tokio-rustls", - "webpki", - "webpki-roots", ] [[package]] @@ -2688,13 +2692,11 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", - "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", - "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2702,12 +2704,10 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", - "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", - "webpki-roots", "winreg 0.6.2", ] @@ -2726,6 +2726,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", + "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2735,16 +2736,20 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", + "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg 0.10.1", ] @@ -2892,15 +2897,33 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "base64 0.10.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2947,9 +2970,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3557,16 +3580,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.10.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", "rustls", - "tokio-io", - "webpki", + "tokio 1.29.1", ] [[package]] @@ -4050,9 +4069,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.4" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", "untrusted", @@ -4060,9 +4079,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.17.0" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index d9fa55ea7..7be68ac78 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -54,3 +54,4 @@ futures = "0.3.28" actix-http = "3.3.1" futures-util = "0.3.28" external = { path = "../external"} +actix-cors = "0.6.4" diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 50f062f9d..fe66a370a 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -67,6 +67,7 @@ async fn main() -> Result<()> { HttpServer::new(move || { App::new() + .wrap(middlewares::cors()) .wrap(logger::GoldenSignalFactory) .wrap(TracingLogger::::new()) .app_data(Data::new(AppState { @@ -117,4 +118,4 @@ async fn main() -> Result<()> { .workers(5) .run() .await -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/context-aware-config/src/middlewares/mod.rs index 2ee97781a..5ee2db878 100644 --- a/crates/context-aware-config/src/middlewares/mod.rs +++ b/crates/context-aware-config/src/middlewares/mod.rs @@ -1 +1,25 @@ pub mod audit_response_header; + +use actix_web::{dev::RequestHead, http::header::HeaderValue}; +pub fn cors() -> actix_cors::Cors { + let origins_env_name = "MJOS_ALLOWED_ORIGINS"; + let allowed_origins: Vec = std::env::var(origins_env_name) + .expect(&format!("{origins_env_name} env")) + .split(",") + .map(String::from) + .collect(); + let validate_origin = move |req_origin: &HeaderValue, _req: &RequestHead| { + if let Ok(req_origin) = req_origin.to_str() { + allowed_origins.contains(&req_origin.into()) + } else { + log::error!("string parsing of req_origin {req_origin:?} failed"); + false + } + }; + actix_cors::Cors::default() + //TODO move this to allowed_origin_fn once middlewares which put tenant + //in request extension are attached + .allowed_origin_fn(validate_origin) + .allow_any_method() + .allow_any_header() +} \ No newline at end of file From 6e01ead030b918b83d93d4d251ae3c5add543fe1 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Mon, 4 Sep 2023 23:06:21 +0530 Subject: [PATCH 134/352] Stabilize & Revert API --- crates/context-aware-config/src/main.rs | 1 + crates/external/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index fe66a370a..b288b5b9b 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -26,6 +26,7 @@ use api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; use experimentation_platform::api::*; +use external::api::*; #[actix_web::main] async fn main() -> Result<()> { diff --git a/crates/external/src/lib.rs b/crates/external/src/lib.rs index bdbd202bd..00472df1b 100644 --- a/crates/external/src/lib.rs +++ b/crates/external/src/lib.rs @@ -1,2 +1,2 @@ pub mod api; -pub use api::external_api::endpoints; \ No newline at end of file +pub use api::external_api::endpoints; From 47b20da444e0fadcc1c2664183a92bf98d3fb1b5 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Thu, 28 Sep 2023 10:10:56 +0530 Subject: [PATCH 135/352] Diff API --- crates/context-aware-config/src/main.rs | 1 - .../external/src/api/external_api/handlers.rs | 39 +++++++++++++++++-- .../external/src/api/external_api/helpers.rs | 27 +++++++++++-- crates/external/src/api/external_api/mod.rs | 1 + crates/external/src/api/external_api/types.rs | 8 ++++ 5 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 crates/external/src/api/external_api/types.rs diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index b288b5b9b..fe66a370a 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -26,7 +26,6 @@ use api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; use experimentation_platform::api::*; -use external::api::*; #[actix_web::main] async fn main() -> Result<()> { diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index abc2a6c81..ef7c496d0 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -1,18 +1,23 @@ use actix_web::{ patch, web::{self, Data, Json}, - Scope, + Scope, get, }; use experimentation_platform::{ api::experiments::{ handlers::{get_experiment, conclude}, - types::{ExperimentResponse, ConcludeExperimentRequest, VariantType} + types::{ExperimentResponse, ConcludeExperimentRequest, VariantType}, + helpers::extract_dimensions }, db::models::Experiment }; -use crate::api::external_api::helpers::fetch_variant_id; +use serde_json::Value; +use crate::api::external_api::{ + helpers::{fetch_variant_id, get_resolved_config}, + types::DiffResponse +}; use service_utils::{ service::types::{AppState, AuthenticationInfo, DbConnection}, types as app, @@ -22,6 +27,7 @@ pub fn endpoints(scope: Scope) -> Scope { scope .service(stabilize) .service(revert) + .service(diff_handler) } #[patch("/{id}/stabilize")] @@ -56,10 +62,35 @@ pub async fn conclude_experiment( let DbConnection(mut conn) = db_conn; let experiment = get_experiment(exp_id, &mut conn)?; - let id = fetch_variant_id(experiment, variant)?; + let id = fetch_variant_id(&experiment, variant)?; let req_body = ConcludeExperimentRequest { chosen_variant : id.to_string() }; let response = conclude (state, exp_id, req_body, conn, auth_info).await?; return Ok(response); } + +#[get("/{id}/diff")] +pub async fn diff_handler( + params: web::Path, + state: Data, + db_conn: DbConnection, +) -> app::Result> { + let DbConnection(mut conn) = db_conn; + let exp_id = params.into_inner(); + let experiment = get_experiment(exp_id, &mut conn)?; + let mut req = extract_dimensions(&experiment.context)?; + let control_id = fetch_variant_id(&experiment, VariantType::CONTROL)?; + let experimental_id = fetch_variant_id(&experiment, VariantType::EXPERIMENTAL)?; + + req.insert("variantIds".to_string(), Value::String(format!("[{}]",control_id))); + let before = get_resolved_config(&state, &req)?; + req.insert("variantIds".to_string(), Value::String(format!("[{}]",experimental_id))); + let after = get_resolved_config(&state, &req)?; + + let res = DiffResponse { + before, + after + }; + return Ok(Json(res)); +} diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs index af93bda2c..89b804a15 100644 --- a/crates/external/src/api/external_api/helpers.rs +++ b/crates/external/src/api/external_api/helpers.rs @@ -1,18 +1,21 @@ +use actix_web::web::Data; use experimentation_platform::{ api::experiments::types::{VariantType, Variant}, db::models::Experiment }; +use serde_json::{Value, Map}; use service_utils::{ - errors::types::Error as err, - types as app, + service::types::AppState, + types as app, errors::types::Error as err, }; pub fn fetch_variant_id( - experiment: Experiment, + experiment: &Experiment, variant: VariantType, ) -> app::Result { - let experiment_variants: Vec = serde_json::from_value(experiment.variants) + let variants = &experiment.variants; + let experiment_variants: Vec = serde_json::from_value(variants.clone()) .map_err(|e| { log::error!("parsing to variant type failed with err: {e}"); err::InternalServerErr("".to_string()) @@ -26,3 +29,19 @@ pub fn fetch_variant_id( log::info!("Failed to fetch variant {:?} id for exp {}", variant, experiment.id); return Err(err::InternalServerErr("".to_string())); } + +pub fn get_resolved_config( + state: &Data, + dimension_ctx : &Map, +) -> app::Result { + let http_client = reqwest::Client::new(); + let url = format!("{}/config/resolve", state.cac_host); + let resp = http_client + .get(&url) + .bearer_auth(&state.admin_token) + .query(dimension_ctx) + .send() + .and_then(|mut resp| resp.json()) + .map_err(|e| err::InternalServerErr(e.to_string()))?; + Ok(resp) +} diff --git a/crates/external/src/api/external_api/mod.rs b/crates/external/src/api/external_api/mod.rs index f98852c0f..61d779e04 100644 --- a/crates/external/src/api/external_api/mod.rs +++ b/crates/external/src/api/external_api/mod.rs @@ -1,3 +1,4 @@ pub mod handlers; pub mod helpers; +pub mod types; pub use handlers::endpoints; \ No newline at end of file diff --git a/crates/external/src/api/external_api/types.rs b/crates/external/src/api/external_api/types.rs new file mode 100644 index 000000000..091960d5c --- /dev/null +++ b/crates/external/src/api/external_api/types.rs @@ -0,0 +1,8 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize)] +pub struct DiffResponse { + pub before: Value, + pub after: Value +} \ No newline at end of file From 5ca8766df57382f94422a6b7381a79f3bae94ed2 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Mon, 25 Sep 2023 13:28:05 +0530 Subject: [PATCH 136/352] feat: added dashboard auth middleware - Validates and authorizes users based on dashboard ACLs - Validates tenant ID in header - Adds an ACL guard for services --- .env.example | 6 +- Cargo.lock | 77 +- Jenkinsfile | 1 + crates/context-aware-config/Cargo.toml | 1 + .../src/api/context/handlers.rs | 41 +- .../src/api/default_config/handlers.rs | 21 +- .../src/api/dimension/handlers.rs | 24 +- crates/context-aware-config/src/main.rs | 2 + crates/experimentation-platform/Cargo.toml | 1 + .../src/api/experiments/handlers.rs | 62 +- crates/external/Cargo.toml | 1 + .../external/src/api/external_api/handlers.rs | 78 +- postman/cac.postman_collection.json | 1625 +++++++++-------- postman/cac/.info.json | 5 +- postman/cac/.meta.json | 2 +- .../cac/{audit_log => audit log}/.meta.json | 2 +- .../list => audit log/get logs}/event.test.js | 0 .../list => audit log/get logs}/request.json | 8 +- postman/cac/audit log/get logs/response.json | 1 + postman/cac/audit_log/list/.event.meta.json | 6 - .../cac/audit_log/list/event.prerequest.js | 1 - postman/cac/config/.meta.json | 3 +- postman/cac/config/resolve/request.json | 20 + postman/cac/config/resolve/response.json | 1 + 24 files changed, 1060 insertions(+), 929 deletions(-) rename postman/cac/{audit_log => audit log}/.meta.json (65%) rename postman/cac/{audit_log/list => audit log/get logs}/event.test.js (100%) rename postman/cac/{audit_log/list => audit log/get logs}/request.json (54%) create mode 100644 postman/cac/audit log/get logs/response.json delete mode 100644 postman/cac/audit_log/list/.event.meta.json delete mode 100644 postman/cac/audit_log/list/event.prerequest.js create mode 100644 postman/cac/config/resolve/request.json create mode 100644 postman/cac/config/resolve/response.json diff --git a/.env.example b/.env.example index 1adce9ffb..a9fa6a67a 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,8 @@ ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" HOSTNAME="---" -MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in \ No newline at end of file +MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in +DASHBOARD_AUTH_ENABLED=false +TENANT_VALIDATION_ENABLED=false +AUTH_EXCLUSION_LIST="GET /context/list,GET /context/{ctx_id},GET /config/resolve,GET /config,GET /audit,GET /experiments,GET /experiments/{id}" +DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" diff --git a/Cargo.lock b/Cargo.lock index f23f9a702..69868ff05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,7 +584,7 @@ dependencies = [ "json-patch", "jsonlogic", "log", - "reqwest 0.11.18", + "reqwest 0.11.20", "serde", "serde_json", ] @@ -712,11 +712,12 @@ dependencies = [ "bytes 1.4.0", "cac_client", "chrono", + "dashboard-auth", "derive_more", "diesel", "diesel-derive-enum", "dotenv", - "env_logger", + "env_logger 0.8.4", "experimentation-platform", "external", "futures 0.3.28", @@ -725,7 +726,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.11.18", + "reqwest 0.11.20", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -954,6 +955,23 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "dashboard-auth" +version = "0.3.3" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.4.1#2162c2e2a2913d8c218990ba904acb28322543c9" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "dotenv", + "env_logger 0.10.0", + "futures-util", + "log", + "reqwest 0.11.20", + "serde", + "serde_json", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1098,6 +1116,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "erased-serde" version = "0.3.30" @@ -1146,13 +1177,14 @@ dependencies = [ "actix", "actix-web", "chrono", + "dashboard-auth", "derive_more", "diesel", "diesel-derive-enum", "dotenv", - "env_logger", + "env_logger 0.8.4", "log", - "reqwest 0.11.18", + "reqwest 0.11.20", "rs-snowflake", "serde", "serde_json", @@ -1169,6 +1201,7 @@ dependencies = [ "actix", "actix-web", "chrono", + "dashboard-auth", "dotenv", "experimentation-platform", "log", @@ -1866,7 +1899,7 @@ dependencies = [ "parking_lot 0.12.1", "percent-encoding 2.2.0", "regex", - "reqwest 0.11.18", + "reqwest 0.11.20", "serde", "serde_json", "time 0.3.21", @@ -2713,9 +2746,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.2", "bytes 1.4.0", @@ -2750,7 +2783,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg 0.10.1", + "winreg 0.50.0", ] [[package]] @@ -3243,7 +3276,7 @@ dependencies = [ "chrono", "dotenv", "jsonlogic", - "reqwest 0.11.18", + "reqwest 0.11.20", "serde", "serde_json", "tokio 1.29.1", @@ -4067,24 +4100,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" @@ -4296,11 +4316,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Jenkinsfile b/Jenkinsfile index daa5b2dd1..d04d99ee3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,6 +14,7 @@ pipeline { AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" GIT_REPO_NAME = "context-aware-config" + CARGO_NET_GIT_FETCH_WITH_CLI=true SSH_AUTH_SOCK = """${sh( returnStdout: true, script: ''' diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 7be68ac78..db8a2f8b5 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -55,3 +55,4 @@ actix-http = "3.3.1" futures-util = "0.3.28" external = { path = "../external"} actix-cors = "0.6.4" +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 85a20093f..5a63bf065 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -16,6 +16,7 @@ use actix_web::{ HttpResponse, Responder, Result, Scope, }; use chrono::Utc; +use dashboard_auth::{middleware::acl, types::User}; use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, @@ -30,12 +31,13 @@ use service_utils::{ pub fn endpoints() -> Scope { Scope::new("") + .guard(acl([("mjos_manager".into(), "RW".into())])) .service(put_handler) - .service(list_contexts) .service(move_handler) - .service(get_context) .service(delete_context) .service(bulk_operations) + .service(list_contexts) + .service(get_context) } type DBConnection = PooledConnection>; @@ -73,7 +75,7 @@ fn val_dimensions_cal_priority( fn create_ctx_from_put_req( req: web::Json, conn: &mut DBConnection, - auth_info: &AuthenticationInfo, + user: &User, ) -> actix_web::Result { let ctx_condition = Value::Object(req.context.to_owned()); let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { @@ -87,7 +89,6 @@ fn create_ctx_from_put_req( }; let context_id = blake3::hash((ctx_condition).to_string().as_bytes()).to_string(); let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); - let AuthenticationInfo(email) = auth_info; Ok(Context { id: context_id.clone(), value: ctx_condition, @@ -95,7 +96,7 @@ fn create_ctx_from_put_req( override_id: override_id.to_owned(), override_: req.r#override.to_owned(), created_at: Utc::now(), - created_by: email.to_owned(), + created_by: user.email.clone(), }) } @@ -138,13 +139,13 @@ enum Error { fn put( req: web::Json, - auth_info: &AuthenticationInfo, + user: &User, conn: &mut PooledConnection>, already_under_txn: bool, ) -> Result { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, auth_info).map_err(|e| { + let new_ctx = create_ctx_from_put_req(req, conn, user).map_err(|e| { log::error!("context struct creation failed with err: {e:?}"); Error::STRTYPE(e.to_string()) })?; @@ -177,13 +178,13 @@ fn put( async fn put_handler( req: web::Json, state: Data, - auth_info: AuthenticationInfo, + user: User, ) -> actix_web::Result> { let conn = &mut state .db_pool .get() .map_err_to_internal_server("unable to get db connection from pool", "")?; - put(req, &auth_info, conn, false) + put(req, &user, conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("context put failed with error: {:?}", e); @@ -194,12 +195,12 @@ async fn put_handler( fn r#move( old_ctx_id: String, req: web::Json, - auth_info: &AuthenticationInfo, + user: &User, conn: &mut PooledConnection>, already_under_txn: bool, ) -> Result { use contexts::dsl; - let new_ctx = create_ctx_from_put_req(req, conn, auth_info).map_err(|e| { + let new_ctx = create_ctx_from_put_req(req, conn, user).map_err(|e| { log::error!("update query failed with error: {e:?}"); Error::STRTYPE(e.to_string()) })?; @@ -258,14 +259,14 @@ async fn move_handler( state: Data, path: Path, req: web::Json, - auth_info: AuthenticationInfo, + user: User, ) -> actix_web::Result> { let conn = &mut state .db_pool .get() .map_err_to_internal_server("unable to get db connection from pool", "")?; - r#move(path.into_inner(), req, &auth_info, conn, false) + r#move(path.into_inner(), req, &user, conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("move api failed with error: {:?}", e); @@ -381,7 +382,7 @@ enum ContextAction { async fn bulk_operations( reqs: web::Json>, state: Data, - auth_info: AuthenticationInfo, + user: User, ) -> actix_web::Result { use contexts::dsl::contexts; let mut conn = state @@ -394,12 +395,8 @@ async fn bulk_operations( for action in reqs.into_inner().into_iter() { match action { ContextAction::PUT(put_req) => { - let resp_result = put( - actix_web::web::Json(put_req), - &auth_info, - transaction_conn, - true, - ); + let resp_result = + put(actix_web::web::Json(put_req), &user, transaction_conn, true); match resp_result { Ok(put_resp) => { @@ -414,7 +411,7 @@ async fn bulk_operations( ContextAction::DELETE(ctx_id) => { let deleted_row = delete(contexts.filter(id.eq(&ctx_id))).execute(transaction_conn); - let AuthenticationInfo(email) = auth_info.clone(); + let email = user.clone().email; match deleted_row { Ok(0) => return Err(diesel::result::Error::RollbackTransaction), Ok(_) => { @@ -431,7 +428,7 @@ async fn bulk_operations( let move_context_resp = r#move( old_ctx_id, actix_web::web::Json(put_req), - &auth_info, + &user, transaction_conn, true, ); diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index c576bc7b5..369987474 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,7 +1,7 @@ use super::types::CreateReq; use crate::{ db::{models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs}, - helpers::validate_jsonschema + helpers::validate_jsonschema, }; use actix_web::{ put, @@ -9,13 +9,19 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; +use dashboard_auth::{ + middleware::acl, + types::User, +}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; -use service_utils::service::types::{AppState, AuthenticationInfo}; +use service_utils::service::types::AppState; pub fn endpoints() -> Scope { - Scope::new("").service(create) + Scope::new("") + .guard(acl([("mjos_manager".into(), "RW".into())])) + .service(create) } #[put("/{key}")] @@ -23,12 +29,11 @@ async fn create( state: Data, key: web::Path, request: web::Json, - auth_info: AuthenticationInfo, + user: User, ) -> HttpResponse { let req = request.into_inner(); let schema = Value::Object(req.schema); - if let Err(e) = - validate_jsonschema(&state.default_config_validation_schema, &schema) + if let Err(e) = validate_jsonschema(&state.default_config_validation_schema, &schema) { return HttpResponse::BadRequest().body(e); }; @@ -52,13 +57,11 @@ async fn create( } }; - let AuthenticationInfo(email) = auth_info; - let new_default_config = DefaultConfig { key: key.into_inner(), value: req.value, schema: schema, - created_by: email, + created_by: user.email, created_at: Utc::now(), }; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 226d5eb99..c8edd4daa 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -9,50 +9,52 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; +use dashboard_auth::{ + middleware::acl, + types::User, +}; use diesel::RunQueryDsl; -use service_utils::service::types::{AppState, AuthenticationInfo}; use jsonschema::{Draft, JSONSchema}; +use service_utils::service::types::AppState; pub fn endpoints() -> Scope { - Scope::new("").service(create) + Scope::new("") + .guard(acl([("mjos_manager".into(), "RW".into())])) + .service(create) } #[put("")] async fn create( state: Data, req: web::Json, - auth_info: AuthenticationInfo, + user: User, ) -> HttpResponse { //TODO move this to the type itself rather than special if check if req.priority <= 0 { return HttpResponse::BadRequest().body("Priority should be greater than 0"); } - let AuthenticationInfo(email) = auth_info; - let create_req = req.into_inner(); let schema_value = create_req.schema; - if let Err(e) = - validate_jsonschema(&state.meta_schema, &schema_value) - { + if let Err(e) = validate_jsonschema(&state.meta_schema, &schema_value) { return HttpResponse::BadRequest().body(e); }; - let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) .compile(&schema_value); if let Err(e) = schema_compile_result { - return HttpResponse::BadRequest().body(String::from(format!("Bad schema: {:?}", e ))); + return HttpResponse::BadRequest() + .body(String::from(format!("Bad schema: {:?}", e))); }; let new_dimension = Dimension { dimension: create_req.dimension, priority: i32::from(create_req.priority), schema: schema_value, - created_by: email, + created_by: user.email, created_at: Utc::now(), }; diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index fe66a370a..a953e8f71 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -4,6 +4,7 @@ mod helpers; mod logger; mod middlewares; +use dashboard_auth::middleware::DashboardAuth; use dotenv; use logger::{init_log_subscriber, CustomRootSpanBuilder}; use std::{env, io::Result}; @@ -67,6 +68,7 @@ async fn main() -> Result<()> { HttpServer::new(move || { App::new() + .wrap(DashboardAuth::default()) .wrap(middlewares::cors()) .wrap(logger::GoldenSignalFactory) .wrap(TracingLogger::::new()) diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 29d8d3488..4165da8db 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -32,3 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 85125585d..38f352a13 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -6,11 +6,19 @@ use actix_web::{ HttpRequest, HttpResponse, Scope, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, r2d2::{PooledConnection, ConnectionManager}, PgConnection}; +use dashboard_auth::{ + middleware::acl, + types::{Tenant, User}, +}; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; + use service_utils::{ errors::types::{Error as err, ErrorResponse}, - service::types::{AppState, AuthenticationInfo, DbConnection}, + service::types::{AppState, DbConnection}, types as app, }; @@ -31,8 +39,9 @@ use crate::{ db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, }; -pub fn endpoints(scope : Scope) -> Scope { +pub fn endpoints(scope: Scope) -> Scope { scope + .guard(acl([("mjos_manager".into(), "RW".into())])) .service(create) .service(conclude_handler) .service(list_experiments) @@ -44,8 +53,9 @@ pub fn endpoints(scope : Scope) -> Scope { async fn create( state: Data, req: web::Json, - auth_info: AuthenticationInfo, + user: User, db_conn: DbConnection, + tenant: Tenant, ) -> app::Result> { use crate::db::schema::cac_v1::experiments::dsl::experiments; let mut variants = req.variants.to_vec(); @@ -128,7 +138,8 @@ async fn create( let created_contexts: Vec = http_client .put(&url) - .bearer_auth(&state.admin_token) + .header("Authorization", format!("Bearer {}", user.token)) + .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() .await @@ -146,10 +157,9 @@ async fn create( } // inserting experiment in db - let AuthenticationInfo(email) = auth_info; let new_experiment = Experiment { id: experiment_id, - created_by: email.to_string(), + created_by: user.email.to_string(), created_at: Utc::now(), last_modified: Utc::now(), name: req.name.to_string(), @@ -158,7 +168,7 @@ async fn create( status: ExperimentStatusType::CREATED, context: req.context.clone(), variants: serde_json::to_value(variants).unwrap(), - last_modified_by: email, + last_modified_by: user.email, chosen_variant: None, }; @@ -178,10 +188,19 @@ async fn conclude_handler( path: web::Path, req: web::Json, db_conn: DbConnection, - auth_info: AuthenticationInfo, + user: User, + tenant: Tenant, ) -> app::Result> { let DbConnection(conn) = db_conn; - let response = conclude(state, path.into_inner(), req.into_inner(), conn, auth_info).await?; + let response = conclude( + state, + path.into_inner(), + req.into_inner(), + conn, + user, + tenant, + ) + .await?; return Ok(Json(ExperimentResponse::from(response))); } @@ -190,7 +209,8 @@ pub async fn conclude( experiment_id: i64, req: ConcludeExperimentRequest, mut conn: PooledConnection>, - auth_info: AuthenticationInfo, + user: User, + tenant: Tenant, ) -> app::Result { use crate::db::schema::cac_v1::experiments::dsl; @@ -255,7 +275,8 @@ pub async fn conclude( let url = state.cac_host.clone() + "/context/bulk-operations"; let response = http_client .put(&url) - .bearer_auth(&state.admin_token) + .header("Authorization", format!("Bearer {}", user.token)) + .header("x-tenant", tenant.as_str()) .json(&operations) .send() .await @@ -268,16 +289,14 @@ pub async fn conclude( ))); } - let AuthenticationInfo(email) = auth_info; - // updating experiment status in db let updated_experiment = diesel::update(dsl::experiments) .filter(dsl::id.eq(experiment_id)) .set(( dsl::status.eq(ExperimentStatusType::CONCLUDED), dsl::last_modified.eq(Utc::now()), - dsl::last_modified_by.eq(email), - dsl::chosen_variant.eq(Some(winner_variant_id)) + dsl::last_modified_by.eq(user.email), + dsl::chosen_variant.eq(Some(winner_variant_id)), )) .get_result::(&mut conn)?; @@ -319,7 +338,8 @@ async fn list_experiments( let now = Utc::now(); builder .filter( - experiments::last_modified.ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + experiments::last_modified + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), ) .filter(experiments::last_modified.le(filters.to_date.unwrap_or(now))) }; @@ -377,7 +397,7 @@ async fn ramp( params: web::Path, req: web::Json, db_conn: DbConnection, - auth_info: AuthenticationInfo, + user: User, ) -> app::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); @@ -411,14 +431,12 @@ async fn ramp( possible_fix: "".to_string(), })); } - let AuthenticationInfo(email) = auth_info; - let new_traffic_percentage = diesel::update(experiments) .filter(id.eq(exp_id)) .set(( traffic_percentage.eq(req.traffic_percentage as i32), last_modified.eq(Utc::now()), - last_modified_by.eq(email), + last_modified_by.eq(user.email), status.eq(ExperimentStatusType::INPROGRESS), )) .execute(&mut conn)?; @@ -432,4 +450,4 @@ async fn ramp( "Traffic percentage has been updated for the experiment id : {}", exp_id ))); -} \ No newline at end of file +} diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index 16db5a103..cee048090 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -22,3 +22,4 @@ chrono = { version = "0.4", features = ["serde"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.9.11"} experimentation-platform = { path = "../experimentation-platform"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index ef7c496d0..3ed91e5a0 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -1,25 +1,25 @@ use actix_web::{ - patch, + get, patch, web::{self, Data, Json}, - Scope, get, + Scope, }; +use dashboard_auth::types::{Tenant, User}; +use crate::api::external_api::{ + helpers::{fetch_variant_id, get_resolved_config}, + types::DiffResponse, +}; use experimentation_platform::{ api::experiments::{ - handlers::{get_experiment, conclude}, - types::{ExperimentResponse, ConcludeExperimentRequest, VariantType}, - helpers::extract_dimensions - }, - db::models::Experiment + handlers::{conclude, get_experiment}, + helpers::extract_dimensions, + types::{ConcludeExperimentRequest, ExperimentResponse, VariantType}, + }, + db::models::Experiment, }; use serde_json::Value; - -use crate::api::external_api::{ - helpers::{fetch_variant_id, get_resolved_config}, - types::DiffResponse -}; use service_utils::{ - service::types::{AppState, AuthenticationInfo, DbConnection}, + service::types::{AppState, DbConnection}, types as app, }; @@ -35,9 +35,18 @@ async fn stabilize( params: web::Path, state: Data, db_conn: DbConnection, - auth_info: AuthenticationInfo, + user: User, + tenant: Tenant, ) -> app::Result> { - let response = conclude_experiment(params.into_inner(), state, db_conn, auth_info, VariantType::EXPERIMENTAL).await?; + let response = conclude_experiment( + params.into_inner(), + state, + db_conn, + user, + tenant, + VariantType::EXPERIMENTAL, + ) + .await?; return Ok(Json(ExperimentResponse::from(response))); } @@ -46,9 +55,18 @@ async fn revert( params: web::Path, state: Data, db_conn: DbConnection, - auth_info: AuthenticationInfo, -) -> app::Result> { - let response = conclude_experiment(params.into_inner(), state, db_conn, auth_info, VariantType::CONTROL).await?; + user: User, + tenant: Tenant, +) -> app::Result> { + let response = conclude_experiment( + params.into_inner(), + state, + db_conn, + user, + tenant, + VariantType::CONTROL, + ) + .await?; return Ok(Json(ExperimentResponse::from(response))); } @@ -56,17 +74,18 @@ pub async fn conclude_experiment( exp_id: i64, state: Data, db_conn: DbConnection, - auth_info: AuthenticationInfo, + user: User, + tenant: Tenant, variant: VariantType, ) -> app::Result { let DbConnection(mut conn) = db_conn; - + let experiment = get_experiment(exp_id, &mut conn)?; let id = fetch_variant_id(&experiment, variant)?; let req_body = ConcludeExperimentRequest { - chosen_variant : id.to_string() + chosen_variant: id.to_string(), }; - let response = conclude (state, exp_id, req_body, conn, auth_info).await?; + let response = conclude(state, exp_id, req_body, conn, user, tenant).await?; return Ok(response); } @@ -83,14 +102,17 @@ pub async fn diff_handler( let control_id = fetch_variant_id(&experiment, VariantType::CONTROL)?; let experimental_id = fetch_variant_id(&experiment, VariantType::EXPERIMENTAL)?; - req.insert("variantIds".to_string(), Value::String(format!("[{}]",control_id))); + req.insert( + "variantIds".to_string(), + Value::String(format!("[{}]", control_id)), + ); let before = get_resolved_config(&state, &req)?; - req.insert("variantIds".to_string(), Value::String(format!("[{}]",experimental_id))); + req.insert( + "variantIds".to_string(), + Value::String(format!("[{}]", experimental_id)), + ); let after = get_resolved_config(&state, &req)?; - let res = DiffResponse { - before, - after - }; + let res = DiffResponse { before, after }; return Ok(Json(res)); } diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json index 9274a3eb1..eaa112be6 100644 --- a/postman/cac.postman_collection.json +++ b/postman/cac.postman_collection.json @@ -1,790 +1,837 @@ { - "item": [ - { - "name": "config", - "item": [ - { - "name": "Get Config", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - " let response = pm.response.json();", - " let expected_response = {", - " \"contexts\": [],", - " \"overrides\": {},", - " \"default_configs\": {}", - " };", - " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response));", - "})", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - } - ], - "url": { - "raw": "{{host}}/config", - "host": [ - "{{host}}" - ], - "path": [ - "config" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Default Config", - "item": [ - { - "name": "Add default-config key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(key, value) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const default_configs = resp_obj.default_configs;", - "", - " console.log(`Checking if key=${key} with value=${value} in default_configs`);", - " pm.expect(default_configs[key]).to.be.eq(value);", - " });", - "}", - "", - "pm.test(\"201 check\", function () {", - " pm.response.to.have.status(201);", - "})", - "", - "pm.test(\"Check if key added to default config\", function () {", - " const key = \"key1\", value = \"value1\";", - " getConfigAndTest(key, value);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"value\":\"value1\",\"schema\":{\"type\":\"string\",\"pattern\":\".*\"}}" - }, - "url": { - "raw": "{{host}}/default-config/key1", - "host": [ - "{{host}}" - ], - "path": [ - "default-config", - "key1" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Dimension", - "item": [ - { - "name": "Create Dimension", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"201 Check\", function () {", - " pm.response.to.have.status(201);", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"schema\":{\"type\":\"string\",\"pattern\":\"^[a-z0-9].*$\"}}" - }, - "url": { - "raw": "{{host}}/dimension", - "host": [ - "{{host}}" - ], - "path": [ - "dimension" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Context", - "item": [ - { - "name": "Create Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - " ", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id]; ", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"piyaz\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"override\":{\"key1\":\"value2\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}" - }, - "url": { - "raw": "{{host}}/context", - "host": [ - "{{host}}" - ], - "path": [ - "context" - ] - } - }, - "response": [] - }, - { - "name": "Update Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - " ", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id]; ", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"piyaz\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}" - }, - "url": { - "raw": "{{host}}/context", - "host": [ - "{{host}}" - ], - "path": [ - "context" - ] - } - }, - "response": [] - }, - { - "name": "Move Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "", - "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", - " const getRequest = {", - " url: `${host}/config`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const resp_obj = response.json();", - " const contexts = resp_obj.contexts;", - " const overrides = resp_obj.overrides;", - "", - " console.log(`Checking if context=${context_id} contexts list.`);", - " const available_context_ids = contexts.map((context) => context.id);", - " pm.expect(available_context_ids).to.include(context_id);", - " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", - " throw \"old context not removed on move\"", - " }", - "", - " const context = contexts.find((context) => context.id === context_id);", - "", - " console.log(`Checking if context condition matches.`);", - " const context_condition = context.condition;", - " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", - " console.log(`Actual => ${JSON.stringify(context_condition)}`);", - " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", - "", - " console.log(`Checking if context=${context_id} uses override=${override_id}`);", - " const context_override_ids = context.override_with_keys;", - " pm.expect(context_override_ids).to.include(override_id);", - "", - "", - " console.log(`Checking override=${override_id} in overrides object`);", - " const override = overrides[override_id];", - " console.log(`Expected => ${JSON.stringify(expected_override)}`);", - " console.log(`Actual => ${JSON.stringify(override)}`);", - " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", - " });", - "}", - "", - "pm.test(\"200 check\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", - " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", - " pm.environment.set(\"context_id\", context_id);", - " pm.environment.set(\"override_id\", override_id);", - "", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Check if context is added\", function () {", - " const response = pm.response.json();", - " const context_id = response.context_id;", - " const override_id = response.override_id;", - "", - " const condition = {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"tamatar\"", - " ]", - " };", - " const override = {", - " \"key1\": \"value2\"", - " };", - "", - "", - " getConfigAndTest(context_id, override_id, condition, override);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"tamatar\"]}}" - }, - "url": { - "raw": "{{host}}/context/move/{{context_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "move", - "{{context_id}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const expected_context = {", - " \"id\": pm.environment.get(\"context_id\"),", - " \"value\": {", - " \"==\": [", - " {", - " \"var\": \"clientId\"", - " },", - " \"tamatar\"", - " ]", - " },", - " \"override_id\": pm.environment.get(\"override_id\"),", - " \"priority\": 100,", - " \"override\": {", - " \"key1\": \"value3\"", - " }", - "};", - "", - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})", - "", - "pm.test(\"Context equality check\", function() {", - " const response = pm.response.json();", - " ", - " delete response.created_at;", - " delete response.created_by;", - "", - " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "url": { - "raw": "{{host}}/context/{{context_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "{{context_id}}" - ] - } - }, - "response": [] - }, - { - "name": "List Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})", - "", - "", - "pm.test(\"Response validation\", function() {", - " const response = pm.response.json();", - " if (response.length == 0) {", - " throw \"list context should return at least one context now\"", - " }", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "url": { - "raw": "{{host}}/context/list", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "list" - ] - } - }, - "response": [] - }, - { - "name": "Delete Context", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.variables.get(\"host\");", - "const context_id = pm.environment.get(\"context_id\");", - "", - "pm.test(\"204 check\", function() {", - " pm.response.to.have.status(204);", - "})", - "", - "pm.test(\"Fetch for context should fail with 404\", function () {", - " const getRequest = {", - " url: `${host}/context/${context_id}`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " console.log(`alloo ${error}`);", - " throw error;", - " }", - "", - " pm.expect(response.code).to.be.eq(404);", - " });", - "})", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "url": { - "raw": "{{host}}/context/{{context_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "context", - "{{context_id}}" - ] - } - }, - "response": [] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "info": { - "_postman_id": "9d2f7da8-68e5-4f9a-b5a8-9abd8f6c8cf0", - "name": "cac", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "variable": [ - { - "key": "host", - "value": "http://localhost:8080", - "type": "default" - }, - { - "key": "token", - "value": "12345678", - "type": "default" - } - ] -} + "info": { + "_postman_id": "0a62ff3e-69d8-455c-8040-f58fe412e0cf", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "17386399" + }, + "item": [ + { + "name": "config", + "item": [ + { + "name": "Get Config", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + " let response = pm.response.json();", + " let expected_response = {", + " \"contexts\": [],", + " \"overrides\": {},", + " \"default_configs\": {}", + " };", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response));", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" + ], + "path": [ + "config" + ] + } + }, + "response": [] + }, + { + "name": "resolve", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/config/resolve?clientId=zee5", + "host": [ + "{{host}}" + ], + "path": [ + "config", + "resolve" + ], + "query": [ + { + "key": "clientId", + "value": "zee5" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default Config", + "item": [ + { + "name": "Add default-config key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(key, value) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const default_configs = resp_obj.default_configs;", + "", + " console.log(`Checking if key=${key} with value=${value} in default_configs`);", + " pm.expect(default_configs[key]).to.be.eq(value);", + " });", + "}", + "", + "pm.test(\"201 check\", function () {", + " pm.response.to.have.status(201);", + "})", + "", + "pm.test(\"Check if key added to default config\", function () {", + " const key = \"key1\", value = \"value1\";", + " getConfigAndTest(key, value);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"value\":\"value1\",\"schema\":{\"type\":\"string\",\"pattern\":\".*\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Dimension", + "item": [ + { + "name": "Create Dimension", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"201 Check\", function () {", + " pm.response.to.have.status(201);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"schema\":{\"type\":\"string\",\"pattern\":\"^[a-z0-9].*$\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Context", + "item": [ + { + "name": "Create Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value2\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Update Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Move Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", + " throw \"old context not removed on move\"", + " }", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + "", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id];", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", + " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"tamatar\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const expected_context = {", + " \"id\": pm.environment.get(\"context_id\"),", + " \"value\": {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " },", + " \"override_id\": pm.environment.get(\"override_id\"),", + " \"priority\": 100,", + " \"override\": {", + " \"key1\": \"value3\"", + " }", + "};", + "", + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Context equality check\", function() {", + " const response = pm.response.json();", + " ", + " delete response.created_at;", + " delete response.created_by;", + "", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "List Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "", + "pm.test(\"Response validation\", function() {", + " const response = pm.response.json();", + " if (response.length == 0) {", + " throw \"list context should return at least one context now\"", + " }", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "list" + ] + } + }, + "response": [] + }, + { + "name": "Delete Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "const context_id = pm.environment.get(\"context_id\");", + "", + "pm.test(\"204 check\", function() {", + " pm.response.to.have.status(204);", + "})", + "", + "pm.test(\"Fetch for context should fail with 404\", function () {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " console.log(`alloo ${error}`);", + " throw error;", + " }", + "", + " pm.expect(response.code).to.be.eq(404);", + " });", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "audit log", + "item": [ + { + "name": "get logs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/audit", + "host": [ + "{{host}}" + ], + "path": [ + "audit" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} \ No newline at end of file diff --git a/postman/cac/.info.json b/postman/cac/.info.json index 6212fbc7f..2ecd05bc5 100644 --- a/postman/cac/.info.json +++ b/postman/cac/.info.json @@ -1,7 +1,8 @@ { "info": { - "_postman_id": "9d2f7da8-68e5-4f9a-b5a8-9abd8f6c8cf0", + "_postman_id": "0a62ff3e-69d8-455c-8040-f58fe412e0cf", "name": "cac", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "17386399" } } diff --git a/postman/cac/.meta.json b/postman/cac/.meta.json index 7c82c21b9..7d010895f 100644 --- a/postman/cac/.meta.json +++ b/postman/cac/.meta.json @@ -4,6 +4,6 @@ "Default Config", "Dimension", "Context", - "audit_log" + "audit log" ] } diff --git a/postman/cac/audit_log/.meta.json b/postman/cac/audit log/.meta.json similarity index 65% rename from postman/cac/audit_log/.meta.json rename to postman/cac/audit log/.meta.json index d7fe61c4d..04e0ac647 100644 --- a/postman/cac/audit_log/.meta.json +++ b/postman/cac/audit log/.meta.json @@ -1,5 +1,5 @@ { "childrenOrder": [ - "list" + "get logs" ] } diff --git a/postman/cac/audit_log/list/event.test.js b/postman/cac/audit log/get logs/event.test.js similarity index 100% rename from postman/cac/audit_log/list/event.test.js rename to postman/cac/audit log/get logs/event.test.js diff --git a/postman/cac/audit_log/list/request.json b/postman/cac/audit log/get logs/request.json similarity index 54% rename from postman/cac/audit_log/list/request.json rename to postman/cac/audit log/get logs/request.json index d3f30e046..c9ef2f644 100644 --- a/postman/cac/audit_log/list/request.json +++ b/postman/cac/audit log/get logs/request.json @@ -1,12 +1,6 @@ { "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - } - ], + "header": [], "url": { "raw": "{{host}}/audit", "host": [ diff --git a/postman/cac/audit log/get logs/response.json b/postman/cac/audit log/get logs/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/audit log/get logs/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/audit_log/list/.event.meta.json b/postman/cac/audit_log/list/.event.meta.json deleted file mode 100644 index 2df9d47d9..000000000 --- a/postman/cac/audit_log/list/.event.meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "eventOrder": [ - "event.prerequest.js", - "event.test.js" - ] -} diff --git a/postman/cac/audit_log/list/event.prerequest.js b/postman/cac/audit_log/list/event.prerequest.js deleted file mode 100644 index 9944ca812..000000000 --- a/postman/cac/audit_log/list/event.prerequest.js +++ /dev/null @@ -1 +0,0 @@ -// any prerequest js code goes here diff --git a/postman/cac/config/.meta.json b/postman/cac/config/.meta.json index 4afa7da94..97068a209 100644 --- a/postman/cac/config/.meta.json +++ b/postman/cac/config/.meta.json @@ -1,5 +1,6 @@ { "childrenOrder": [ - "Get Config" + "Get Config", + "resolve" ] } diff --git a/postman/cac/config/resolve/request.json b/postman/cac/config/resolve/request.json new file mode 100644 index 000000000..c962d39c8 --- /dev/null +++ b/postman/cac/config/resolve/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/config/resolve?clientId=zee5", + "host": [ + "{{host}}" + ], + "path": [ + "config", + "resolve" + ], + "query": [ + { + "key": "clientId", + "value": "zee5" + } + ] + } +} diff --git a/postman/cac/config/resolve/response.json b/postman/cac/config/resolve/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/config/resolve/response.json @@ -0,0 +1 @@ +[] From 150a7bed9c7034d77488fd007313c3cce87cd08b Mon Sep 17 00:00:00 2001 From: Jenkins Date: Thu, 5 Oct 2023 05:45:35 +0000 Subject: [PATCH 137/352] chore(version): v0.7.0 [skip ci] --- CHANGELOG.md | 13 +++ Cargo.lock | 96 +++++++++++--------- crates/context-aware-config/CHANGELOG.md | 7 ++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++ crates/experimentation-platform/Cargo.toml | 4 +- 6 files changed, 82 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9afa03f0..7b1d27e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.7.0 - 2023-10-05 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.4.0 +- context-aware-config bumped to context-aware-config-v0.6.0 +### Global changes +#### Continuous Integration +- **(flake.nix)** pin nodejs version to 18 in flake - (614261e) - Natarajan Kannan +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra +- PICAF-24664 cors middleware attached - (8cb4805) - Ritick Madaan + +- - - + ## v0.6.1 - 2023-09-20 ### Package updates - cac_client bumped to cac_client-v0.2.1 diff --git a/Cargo.lock b/Cargo.lock index 69868ff05..2b274c2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.5.1" +version = "0.6.0" dependencies = [ "actix", "actix-cors", @@ -726,7 +726,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.11.20", + "reqwest 0.9.24", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -911,6 +911,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1172,7 +1181,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.3.1" +version = "0.4.0" dependencies = [ "actix", "actix-web", @@ -1666,16 +1675,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" dependencies = [ - "futures-util", - "http 0.2.9", - "hyper 0.14.26", + "bytes 0.4.12", + "ct-logs", + "futures 0.1.31", + "hyper 0.12.36", "rustls", - "tokio 1.29.1", + "tokio-io", "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -2725,11 +2737,13 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", + "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", + "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2737,10 +2751,12 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", + "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", + "webpki-roots", "winreg 0.6.2", ] @@ -2759,7 +2775,6 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", - "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2769,20 +2784,16 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", - "rustls", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", - "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg 0.50.0", ] @@ -2930,33 +2941,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ + "base64 0.10.1", "log", "ring", - "rustls-webpki", "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" -dependencies = [ - "ring", - "untrusted", + "webpki", ] [[package]] @@ -3003,9 +2996,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -3613,12 +3606,16 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", "rustls", - "tokio 1.29.1", + "tokio-io", + "webpki", ] [[package]] @@ -4100,11 +4097,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +dependencies = [ + "webpki", +] [[package]] name = "winapi" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 69babcfb0..0c89914f0 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.6.0 - 2023-10-05 +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra +- PICAF-24664 cors middleware attached - (8cb4805) - Ritick Madaan + +- - - + ## context-aware-config-v0.5.1 - 2023-09-20 #### Bug Fixes - PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index db8a2f8b5..ea4aa5c8a 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.5.1" +version = "0.6.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 0c298be57..30723663b 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.4.0 - 2023-10-05 +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.3.1 - 2023-09-12 #### Bug Fixes - failed build due to untracked schema.rs file changes - (5bc4eae) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 4165da8db..63c57fce4 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.3.1" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -32,4 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} From 3da23dadff17bda2a5b62b10b45034984b966170 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 5 Oct 2023 12:30:03 +0530 Subject: [PATCH 138/352] fix: ssh.bitbucket.juspay.net added to known hosts in docker bulid - this is needed because the application now uses git dependencies - fixed cac tests dir as well --- Dockerfile | 4 +++- Jenkinsfile | 3 ++- makefile | 4 ++-- postman/cac.postman_collection.json | 18 ++++++++++++++++-- postman/cac/.info.json | 4 ++-- .../cac/audit log/get logs/.event.meta.json | 5 +++++ postman/cac/audit log/get logs/event.test.js | 4 +--- 7 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 postman/cac/audit log/get logs/.event.meta.json diff --git a/Dockerfile b/Dockerfile index 8f6bf7a5f..5918fd64b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ FROM rust:1.67 as builder WORKDIR /build COPY . . +RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts +RUN --mount=type=ssh cargo build --release RUN cargo build --release FROM debian:bullseye-slim @@ -13,4 +15,4 @@ RUN apt-get update && apt-get install -y libpq5 ca-certificates COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT -CMD ["/app/context-aware-config"] \ No newline at end of file +CMD ["/app/context-aware-config"] diff --git a/Jenkinsfile b/Jenkinsfile index d04d99ee3..1102e61c4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -127,7 +127,8 @@ pipeline { steps { sh '''make ci-build -e \ VERSION=${NEW_SEMANTIC_VERSION} \ - SOURCE_COMMIT=${COMMIT_HASH} + SOURCE_COMMIT=${COMMIT_HASH} \ + SSH_AUTH_SOCK=${SSH_AUTH_SOCK} ''' } } diff --git a/makefile b/makefile index 1cdd6728a..bbd48dfe0 100644 --- a/makefile +++ b/makefile @@ -59,7 +59,7 @@ ci-test: npm run test ci-build: - docker build \ + docker buildx build --ssh default=$(SSH_AUTH_SOCK) \ -t $(IMAGE_NAME):$(VERSION) \ --build-arg "CONTEXT_AWARE_CONFIG_VERSION=${VERSION}" \ --build-arg "SOURCE_COMMIT=${SOURCE_COMMIT}" \ @@ -76,4 +76,4 @@ registry-login: --password-stdin $(REGISTRY_HOST) -default: dev-build +default: dev-build \ No newline at end of file diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json index eaa112be6..6c8f01671 100644 --- a/postman/cac.postman_collection.json +++ b/postman/cac.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "0a62ff3e-69d8-455c-8040-f58fe412e0cf", + "_postman_id": "12a7fe9f-2a54-4afa-aa48-4625bfc8e858", "name": "cac", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "17386399" + "_exporter_id": "15880623" }, "item": [ { @@ -784,6 +784,20 @@ "item": [ { "name": "get logs", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expect response be 200', function () {", + " pm.response.to.be.ok;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "GET", "header": [], diff --git a/postman/cac/.info.json b/postman/cac/.info.json index 2ecd05bc5..59d37a94c 100644 --- a/postman/cac/.info.json +++ b/postman/cac/.info.json @@ -1,8 +1,8 @@ { "info": { - "_postman_id": "0a62ff3e-69d8-455c-8040-f58fe412e0cf", + "_postman_id": "12a7fe9f-2a54-4afa-aa48-4625bfc8e858", "name": "cac", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "17386399" + "_exporter_id": "15880623" } } diff --git a/postman/cac/audit log/get logs/.event.meta.json b/postman/cac/audit log/get logs/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/audit log/get logs/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/audit log/get logs/event.test.js b/postman/cac/audit log/get logs/event.test.js index 7ad11654a..f0145ea58 100644 --- a/postman/cac/audit log/get logs/event.test.js +++ b/postman/cac/audit log/get logs/event.test.js @@ -1,5 +1,3 @@ -/* global pm */ - pm.test('expect response be 200', function () { pm.response.to.be.ok; -}); \ No newline at end of file +}); From 1ac80c4644242d9fc3793e3483bc99bffe3febcf Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Thu, 5 Oct 2023 15:12:13 +0530 Subject: [PATCH 139/352] fix: add user struct in delete context API --- .../src/api/context/handlers.rs | 7 ++- crates/service-utils/src/service/types.rs | 51 ------------------- postman/cac.postman_collection.json | 2 +- postman/cac/audit log/.meta.json | 2 +- .../{get logs => get_logs}/.event.meta.json | 0 .../{get logs => get_logs}/event.test.js | 0 .../{get logs => get_logs}/request.json | 0 .../{get logs => get_logs}/response.json | 0 8 files changed, 5 insertions(+), 57 deletions(-) rename postman/cac/audit log/{get logs => get_logs}/.event.meta.json (100%) rename postman/cac/audit log/{get logs => get_logs}/event.test.js (100%) rename postman/cac/audit log/{get logs => get_logs}/request.json (100%) rename postman/cac/audit log/{get logs => get_logs}/response.json (100%) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 5a63bf065..bef60d3fb 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -26,7 +26,7 @@ use diesel::{ use serde_json::{json, Value, Value::Null}; use service_utils::{ helpers::ToActixErr, - service::types::{AppState, AuthenticationInfo}, + service::types::AppState, }; pub fn endpoints() -> Scope { @@ -347,7 +347,7 @@ async fn list_contexts( async fn delete_context( state: Data, path: Path, - auth_info: AuthenticationInfo, + user: User, ) -> actix_web::Result { use contexts::dsl; let mut conn = state @@ -357,11 +357,10 @@ async fn delete_context( let ctx_id = path.into_inner(); let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(&mut conn); - let AuthenticationInfo(email) = auth_info; match deleted_row { Ok(0) => Err(ErrorNotFound("")), Ok(_) => { - log::info!("{ctx_id} context deleted by {email}"); + log::info!("{ctx_id} context deleted by {}", user.email); Ok(HttpResponse::NoContent().finish()) } Err(e) => { diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 4052203e8..ff5dcf26d 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -28,57 +28,6 @@ pub struct AppState { pub snowflake_generator: Mutex, } -#[derive(Clone)] -pub struct AuthenticationInfo(pub String); -impl FromRequest for AuthenticationInfo { - type Error = Error; - type Future = Ready>; - - fn from_request( - req: &actix_web::HttpRequest, - _: &mut actix_web::dev::Payload, - ) -> Self::Future { - let opt_token = req - .headers() - .get("Authorization") - .and_then(|h| h.to_str().ok()) - .and_then(|h| { - if h.starts_with("Bearer") { - Some(h) - } else { - None - } - }) - .and_then(|h| { - h.split(' ') - .collect::>() - .get(1) - .map(|token| token.to_string()) - }); - - let opt_admin_token = req - .app_data() - .map(|d: &Data| d.admin_token.as_str()); - - let result = match (opt_token, opt_admin_token) { - (_, None) => { - log::info!("ERROR: ADMIN TOKEN NOT FOUND!!!!"); - Err(error::ErrorInternalServerError("")) - } - (None, _) => Err(error::ErrorUnauthorized("Bearer token required.")), - (Some(token), Some(admin_token)) if token != admin_token => { - Err(error::ErrorUnauthorized("")) - } - (Some(_token), Some(_admin_token)) => { - let email = "cac.admin@juspay.in"; - let auth_info = AuthenticationInfo(email.to_string()); - Ok(auth_info) - } - }; - ready(result) - } -} - pub struct DbConnection(pub PooledConnection>); impl FromRequest for DbConnection { type Error = Error; diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json index 6c8f01671..72101baa4 100644 --- a/postman/cac.postman_collection.json +++ b/postman/cac.postman_collection.json @@ -783,7 +783,7 @@ "name": "audit log", "item": [ { - "name": "get logs", + "name": "get_logs", "event": [ { "listen": "test", diff --git a/postman/cac/audit log/.meta.json b/postman/cac/audit log/.meta.json index 04e0ac647..6d5cafaff 100644 --- a/postman/cac/audit log/.meta.json +++ b/postman/cac/audit log/.meta.json @@ -1,5 +1,5 @@ { "childrenOrder": [ - "get logs" + "get_logs" ] } diff --git a/postman/cac/audit log/get logs/.event.meta.json b/postman/cac/audit log/get_logs/.event.meta.json similarity index 100% rename from postman/cac/audit log/get logs/.event.meta.json rename to postman/cac/audit log/get_logs/.event.meta.json diff --git a/postman/cac/audit log/get logs/event.test.js b/postman/cac/audit log/get_logs/event.test.js similarity index 100% rename from postman/cac/audit log/get logs/event.test.js rename to postman/cac/audit log/get_logs/event.test.js diff --git a/postman/cac/audit log/get logs/request.json b/postman/cac/audit log/get_logs/request.json similarity index 100% rename from postman/cac/audit log/get logs/request.json rename to postman/cac/audit log/get_logs/request.json diff --git a/postman/cac/audit log/get logs/response.json b/postman/cac/audit log/get_logs/response.json similarity index 100% rename from postman/cac/audit log/get logs/response.json rename to postman/cac/audit log/get_logs/response.json From a9e50be01be791fd6750ae18105c92b23f22959e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Thu, 5 Oct 2023 10:03:08 +0000 Subject: [PATCH 140/352] chore(version): v0.7.1 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1d27e61..1e5409306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.7.1 - 2023-10-05 +### Package updates +- service-utils bumped to service-utils-v0.4.1 +- context-aware-config bumped to context-aware-config-v0.6.1 +### Global changes +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra +- ssh.bitbucket.juspay.net added to known hosts in docker bulid - (9207701) - Ritick Madaan + +- - - + ## v0.7.0 - 2023-10-05 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 2b274c2ef..fea2ab7ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.6.0" +version = "0.6.1" dependencies = [ "actix", "actix-cors", @@ -3114,7 +3114,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.4.0" +version = "0.4.1" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 0c89914f0..7d1ff151c 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.6.1 - 2023-10-05 +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra + +- - - + ## context-aware-config-v0.6.0 - 2023-10-05 #### Features - [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index ea4aa5c8a..af3f79580 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.6.0" +version = "0.6.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index d2b45ca60..6c680c7c9 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.4.1 - 2023-10-05 +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra + +- - - + ## service-utils-v0.4.0 - 2023-09-12 #### Features - Schema addition for Dimension values - (7960a67) - Prasanna P diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index abc087997..fb5ec21bc 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.4.0" +version = "0.4.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 1337d346e9c3f79f177c5e910e3c4543d2523e53 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Thu, 5 Oct 2023 17:34:02 +0530 Subject: [PATCH 141/352] chore: database migration for dimensions table - variantIds brought to least priority - toss, scope dimensions deleted --- .../2023-10-05-095010_dimensions-priority-update/down.sql | 7 +++++++ .../v1/2023-10-05-095010_dimensions-priority-update/up.sql | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql create mode 100644 crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql diff --git a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql new file mode 100644 index 000000000..1c2134c45 --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql @@ -0,0 +1,7 @@ +UPDATE cac_v1.dimensions SET priority=1 where dimension='os'; +UPDATE cac_v1.dimensions SET priority=32 where dimension='internalUser'; +UPDATE cac_v1.dimensions SET priority=64 where dimension='variantIds'; +INSERT INTO cac_v1.dimensions (dimension, priority, created_by, schema) + VALUES ('toss', '2', 'migration_down@juspay.in', '{"type":"number"}'); +INSERT INTO cac_v1.dimensions (dimension, priority, created_by, schema) + VALUES ('scope', '16', 'migration_down@juspay.in', '{"enum":["beta","release","cug"],"type":"string"}'); \ No newline at end of file diff --git a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql new file mode 100644 index 000000000..c85b5b1ee --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql @@ -0,0 +1,5 @@ +DELETE FROM cac_v1.dimensions where dimension='toss'; +DELETE FROM cac_v1.dimensions where dimension='scope'; +UPDATE cac_v1.dimensions SET priority=1 where dimension='variantIds'; +UPDATE cac_v1.dimensions SET priority=2 where dimension='os'; +UPDATE cac_v1.dimensions SET priority=16 where dimension='internalUser'; \ No newline at end of file From 585a15c28ba12351347d8952a624e707a4aa89e7 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 9 Oct 2023 13:10:53 +0530 Subject: [PATCH 142/352] feat: server's keep-alive time and db connection pool max size made configurable --- .env.example | 2 + Cargo.lock | 96 +++++++++---------- .../src/api/audit_log/mod.rs | 2 +- .../src/api/context/handlers.rs | 5 +- .../src/api/default_config/handlers.rs | 5 +- .../src/api/dimension/handlers.rs | 5 +- crates/context-aware-config/src/api/mod.rs | 2 +- crates/context-aware-config/src/main.rs | 34 +++---- .../src/middlewares/mod.rs | 2 +- crates/service-utils/src/db/utils.rs | 1 + 10 files changed, 69 insertions(+), 85 deletions(-) diff --git a/.env.example b/.env.example index a9fa6a67a..cc5bf10cc 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,5 @@ DASHBOARD_AUTH_ENABLED=false TENANT_VALIDATION_ENABLED=false AUTH_EXCLUSION_LIST="GET /context/list,GET /context/{ctx_id},GET /config/resolve,GET /config,GET /audit,GET /experiments,GET /experiments/{id}" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" +ACTIX_KEEP_ALIVE=120 +MAX_DB_CONNECTION_POOL_SIZE=3 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fea2ab7ad..4db11e5b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,9 +362,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -726,7 +726,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.9.24", + "reqwest 0.11.20", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -911,15 +911,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "cxx" version = "1.0.94" @@ -1675,19 +1666,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.17.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes 0.4.12", - "ct-logs", - "futures 0.1.31", - "hyper 0.12.36", + "futures-util", + "http 0.2.9", + "hyper 0.14.26", "rustls", - "tokio-io", + "tokio 1.29.1", "tokio-rustls", - "webpki", - "webpki-roots", ] [[package]] @@ -2737,13 +2725,11 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", - "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", - "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2751,12 +2737,10 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", - "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", - "webpki-roots", "winreg 0.6.2", ] @@ -2775,6 +2759,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", + "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2784,16 +2769,20 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", + "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg 0.50.0", ] @@ -2941,15 +2930,33 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "base64 0.10.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2996,9 +3003,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3606,16 +3613,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.10.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", "rustls", - "tokio-io", - "webpki", + "tokio 1.29.1", ] [[package]] @@ -4097,24 +4100,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.17.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" diff --git a/crates/context-aware-config/src/api/audit_log/mod.rs b/crates/context-aware-config/src/api/audit_log/mod.rs index 5332ce406..9e1802e17 100644 --- a/crates/context-aware-config/src/api/audit_log/mod.rs +++ b/crates/context-aware-config/src/api/audit_log/mod.rs @@ -1,4 +1,4 @@ mod handlers; mod types; -pub use handlers::endpoints; \ No newline at end of file +pub use handlers::endpoints; diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index bef60d3fb..ca99bf84e 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -24,10 +24,7 @@ use diesel::{ Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; use serde_json::{json, Value, Value::Null}; -use service_utils::{ - helpers::ToActixErr, - service::types::AppState, -}; +use service_utils::{helpers::ToActixErr, service::types::AppState}; pub fn endpoints() -> Scope { Scope::new("") diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 369987474..195f9f48e 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -9,10 +9,7 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::{ - middleware::acl, - types::User, -}; +use dashboard_auth::{middleware::acl, types::User}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index c8edd4daa..9a42b5524 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -9,10 +9,7 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::{ - middleware::acl, - types::User, -}; +use dashboard_auth::{middleware::acl, types::User}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use service_utils::service::types::AppState; diff --git a/crates/context-aware-config/src/api/mod.rs b/crates/context-aware-config/src/api/mod.rs index d1a8fa68d..a247540be 100644 --- a/crates/context-aware-config/src/api/mod.rs +++ b/crates/context-aware-config/src/api/mod.rs @@ -1,5 +1,5 @@ +pub mod audit_log; pub mod config; pub mod context; pub mod default_config; pub mod dimension; -pub mod audit_log; diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index a953e8f71..bb131504e 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -4,29 +4,24 @@ mod helpers; mod logger; mod middlewares; +use crate::middlewares::audit_response_header::{AuditHeader, TableName}; +use actix_web::{web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; +use api::*; use dashboard_auth::middleware::DashboardAuth; use dotenv; +use experimentation_platform::api::*; +use helpers::{get_default_config_validation_schema, get_meta_schema}; use logger::{init_log_subscriber, CustomRootSpanBuilder}; -use std::{env, io::Result}; -use tracing::{span, Level}; - -use actix_web::{web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; -use snowflake::SnowflakeIdGenerator; -use std::sync::Mutex; -use tracing_actix_web::TracingLogger; - use service_utils::{ db::utils::get_pool, helpers::{get_from_env_unsafe, get_pod_info}, service::types::{AppState, ExperimentationFlags}, }; - -use crate::middlewares::audit_response_header::{AuditHeader, TableName}; - -use api::*; -use helpers::{get_default_config_validation_schema, get_meta_schema}; - -use experimentation_platform::api::*; +use snowflake::SnowflakeIdGenerator; +use std::{env, io::Result}; +use std::{sync::Mutex, time::Duration}; +use tracing::{span, Level}; +use tracing_actix_web::TracingLogger; #[actix_web::main] async fn main() -> Result<()> { @@ -114,10 +109,15 @@ async fn main() -> Result<()> { .service(config::endpoints()), ) .service(scope("/audit").service(audit_log::endpoints())) - .service(external::endpoints(experiments::endpoints(scope("/experiments")))) + .service(external::endpoints(experiments::endpoints(scope( + "/experiments", + )))) }) .bind(("0.0.0.0", 8080))? .workers(5) + .keep_alive(Duration::from_secs( + get_from_env_unsafe("ACTIX_KEEP_ALIVE").unwrap_or(120), + )) .run() .await -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/context-aware-config/src/middlewares/mod.rs index 5ee2db878..f75575139 100644 --- a/crates/context-aware-config/src/middlewares/mod.rs +++ b/crates/context-aware-config/src/middlewares/mod.rs @@ -22,4 +22,4 @@ pub fn cors() -> actix_cors::Cors { .allowed_origin_fn(validate_origin) .allow_any_method() .allow_any_header() -} \ No newline at end of file +} diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index 6d7426b0d..5f883ed14 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -21,6 +21,7 @@ pub async fn get_pool() -> Pool> { let manager: ConnectionManager = ConnectionManager::::new(db_url); Pool::builder() + .max_size(get_from_env_unsafe("MAX_DB_CONNECTION_POOL_SIZE").unwrap_or(3)) .build(manager) .expect("Error building a connection pool") } From ee5e00c6bd08c6ce624664bc244ffc018d09bf9a Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 9 Oct 2023 08:03:59 +0000 Subject: [PATCH 143/352] chore(version): v0.8.0 [skip ci] --- CHANGELOG.md | 10 +++ Cargo.lock | 96 +++++++++++++----------- crates/context-aware-config/CHANGELOG.md | 8 ++ crates/context-aware-config/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++ crates/service-utils/Cargo.toml | 2 +- 6 files changed, 79 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5409306..91abfcca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.8.0 - 2023-10-09 +### Package updates +- context-aware-config bumped to context-aware-config-v0.7.0 +- service-utils bumped to service-utils-v0.5.0 +### Global changes +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan + +- - - + ## v0.7.1 - 2023-10-05 ### Package updates - service-utils bumped to service-utils-v0.4.1 diff --git a/Cargo.lock b/Cargo.lock index 4db11e5b8..a187bdf89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.6.1" +version = "0.7.0" dependencies = [ "actix", "actix-cors", @@ -726,7 +726,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.11.20", + "reqwest 0.9.24", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -911,6 +911,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1666,16 +1675,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" dependencies = [ - "futures-util", - "http 0.2.9", - "hyper 0.14.26", + "bytes 0.4.12", + "ct-logs", + "futures 0.1.31", + "hyper 0.12.36", "rustls", - "tokio 1.29.1", + "tokio-io", "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -2725,11 +2737,13 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", + "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", + "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2737,10 +2751,12 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", + "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", + "webpki-roots", "winreg 0.6.2", ] @@ -2759,7 +2775,6 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", - "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2769,20 +2784,16 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", - "rustls", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", - "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg 0.50.0", ] @@ -2930,33 +2941,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ + "base64 0.10.1", "log", "ring", - "rustls-webpki", "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" -dependencies = [ - "ring", - "untrusted", + "webpki", ] [[package]] @@ -3003,9 +2996,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -3121,7 +3114,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.4.1" +version = "0.5.0" dependencies = [ "actix", "actix-web", @@ -3613,12 +3606,16 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", "rustls", - "tokio 1.29.1", + "tokio-io", + "webpki", ] [[package]] @@ -4100,11 +4097,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +dependencies = [ + "webpki", +] [[package]] name = "winapi" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 7d1ff151c..a4901cbd8 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.7.0 - 2023-10-09 +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan +#### Miscellaneous Chores +- database migration for dimensions table - (3a36c56) - Ritick Madaan + +- - - + ## context-aware-config-v0.6.1 - 2023-10-05 #### Bug Fixes - [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index af3f79580..6263c89cc 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.6.1" +version = "0.7.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 6c680c7c9..9b1bc11cd 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.5.0 - 2023-10-09 +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan + +- - - + ## service-utils-v0.4.1 - 2023-10-05 #### Bug Fixes - [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index fb5ec21bc..c2cff0948 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.4.1" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 7b6ae82c881fb6b1fb77c2a4ec1cb872f3155aa7 Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Mon, 9 Oct 2023 15:55:50 +0530 Subject: [PATCH 144/352] fix: add migration for changing default_configs_keys --- .../2023-10-09-101000_delete_old_default_configs_keys/down.sql | 1 + .../v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql create mode 100644 crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql diff --git a/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql b/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql new file mode 100644 index 000000000..291a97c5c --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql b/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql new file mode 100644 index 000000000..dfb5e9dcb --- /dev/null +++ b/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +delete from cac_v1.default_configs where key in ('dependency_hyperpay','ec_base_html_android', 'ec_base_html_ios', 'godel_acs_android', 'godel_bundle_android', 'godel_config_android', 'godel_placeholder_acs_android', 'godel_placeholder_bundle_android', 'godel_placeholder_config_android', 'hyperos_placeholder_tracker_android', 'hyperos_placeholder_tracker_ios', 'hyperos_placeholder_tracker_web', 'hyperpay_base_html_android', 'hyperpay_base_html_ios', 'patch_entries', 'upi_base_html_ios', 'upi_intent_base_html_android', 'upi_intent_bundle_web', 'upi_intent_config_web'); \ No newline at end of file From 189c3696344203ecb434005ff8b52a6de2bddf67 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 10 Oct 2023 15:43:57 +0000 Subject: [PATCH 145/352] chore(version): v0.8.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91abfcca8..3007b40e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.8.1 - 2023-10-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.7.1 +### Global changes + +- - - + ## v0.8.0 - 2023-10-09 ### Package updates - context-aware-config bumped to context-aware-config-v0.7.0 diff --git a/Cargo.lock b/Cargo.lock index a187bdf89..b9cb25c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index a4901cbd8..04016f7fc 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.7.1 - 2023-10-10 +#### Bug Fixes +- PICAF-24742 add migration for changing default_configs_keys - (55f8895) - Pratik Mishra + +- - - + ## context-aware-config-v0.7.0 - 2023-10-09 #### Features - server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 6263c89cc..da61217fa 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 71c7a25cc752ea62e36cab340080f82be080802a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 28 Aug 2023 12:37:20 +0530 Subject: [PATCH 146/352] feat: support to update experiment override_keys and variants --- .../src/api/context/handlers.rs | 23 +- .../src/api/context/types.rs | 14 + .../src/api/experiments/handlers.rs | 240 ++++++++++++++++-- .../src/api/experiments/helpers.rs | 134 ++++++---- .../src/api/experiments/types.rs | 27 +- 5 files changed, 348 insertions(+), 90 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index ca99bf84e..e97418646 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,5 +1,5 @@ use crate::{ - api::context::types::{PaginationParams, PutReq, PutResp}, + api::context::types::{PaginationParams, PutReq, PutResp, ContextAction, ContextBulkResponse}, db::{ models::{Context, Dimension}, schema::cac_v1::{ @@ -367,26 +367,19 @@ async fn delete_context( } } -#[derive(serde::Deserialize)] -enum ContextAction { - PUT(PutReq), - DELETE(String), - MOVE((String, PutReq)), -} - #[put("/bulk-operations")] async fn bulk_operations( reqs: web::Json>, state: Data, user: User, -) -> actix_web::Result { +) -> actix_web::Result>> { use contexts::dsl::contexts; let mut conn = state .db_pool .get() .map_err_to_internal_server("Unable to get db connection from pool", "")?; - let mut resp = Vec::::new(); + let mut resp = Vec::::new(); let result = conn.transaction::<_, diesel::result::Error, _>(|transaction_conn| { for action in reqs.into_inner().into_iter() { match action { @@ -396,7 +389,7 @@ async fn bulk_operations( match resp_result { Ok(put_resp) => { - resp.push(json!(put_resp)); + resp.push(ContextBulkResponse::PUT(put_resp)); } Err(e) => { log::error!("Failed at insert into contexts due to {:?}", e); @@ -412,7 +405,7 @@ async fn bulk_operations( Ok(0) => return Err(diesel::result::Error::RollbackTransaction), Ok(_) => { log::info!("{ctx_id} context deleted by {email}"); - resp.push(json!(format!("{ctx_id} deleted succesfully"))) + resp.push(ContextBulkResponse::DELETE(format!("{ctx_id} deleted succesfully"))) } Err(e) => { log::error!("Delete context failed due to {:?}", e); @@ -430,7 +423,7 @@ async fn bulk_operations( ); match move_context_resp { - Ok(move_resp) => resp.push(json!(move_resp)), + Ok(move_resp) => resp.push(ContextBulkResponse::MOVE(move_resp)), Err(e) => { log::error!( "Failed at moving context reponse due to {:?}", @@ -446,7 +439,7 @@ async fn bulk_operations( }); match result { - Ok(_) => Ok(HttpResponse::Ok().json(resp)), // If the transaction was successful, return the responses + Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index 03b1e101b..b25f73cef 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -19,3 +19,17 @@ pub struct PaginationParams { pub page: Option, pub size: Option, } + +#[derive(serde::Deserialize)] +pub enum ContextAction { + PUT(PutReq), + DELETE(String), + MOVE((String, PutReq)), +} + +#[derive(serde::Serialize)] +pub enum ContextBulkResponse { + PUT(PutResp), + DELETE(String), + MOVE(PutResp), +} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 38f352a13..b406c9292 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use actix_web::{ - get, patch, post, + get, patch, post, put, web::{self, Data, Json, Query}, HttpRequest, HttpResponse, Scope, }; @@ -28,9 +28,10 @@ use super::{ check_variants_override_coverage, validate_experiment, }, types::{ - ConcludeExperimentRequest, ContextAction, ContextPutReq, ContextPutResp, - ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, - ExperimentsResponse, ListFilters, RampRequest, Variant, + ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextPutReq, + ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, + ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, + RampRequest, Variant, }, }; @@ -39,7 +40,9 @@ use crate::{ db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, }; -pub fn endpoints(scope: Scope) -> Scope { +use serde_json::{Value, Map, json}; + +pub fn endpoints(scope : Scope) -> Scope { scope .guard(acl([("mjos_manager".into(), "RW".into())])) .service(create) @@ -47,6 +50,7 @@ pub fn endpoints(scope: Scope) -> Scope { .service(list_experiments) .service(get_experiment_handler) .service(ramp) + .service(update_override_keys) } #[post("")] @@ -78,7 +82,8 @@ async fn create( check_variant_types(&variants)?; // Checking if all the variants are overriding the mentioned keys - let are_valid_variants = check_variants_override_coverage(&variants, override_keys); + let variant_overrides = variants.iter().map(|variant| variant.overrides.clone()).collect::>>(); + let are_valid_variants = check_variants_override_coverage(&variant_overrides, override_keys); if !are_valid_variants { return Err(err::BadRequest(ErrorResponse { message: "all variants should contain the keys mentioned in override_keys" @@ -97,7 +102,8 @@ async fn create( // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; - let (valid, reason) = validate_experiment(&req, &flags, &mut conn)?; + let (valid, reason) = + validate_experiment(&req.context, &req.override_keys, None, &flags, &mut conn)?; if !valid { return Err(err::BadRequest(ErrorResponse { message: reason, @@ -127,7 +133,7 @@ async fn create( "Could not convert updated CAC context to serde Object".to_string(), ))? .clone(), - r#override: variant.overrides.clone(), + r#override: json!(variant.overrides), }; cac_operations.push(ContextAction::PUT(payload)); } @@ -144,9 +150,17 @@ async fn create( .send() .await .map_err(|e| err::InternalServerErr(e.to_string()))? - .json::>() + .json::>() .await - .map_err(|e| err::InternalServerErr(e.to_string()))?; + .map_err(|e| err::InternalServerErr(e.to_string()))? + .into_iter() + .map(|response| match response { + ContextBulkResponse::PUT(created_context) => Some(created_context), + _ => None, + }) + .filter(|response| matches!(response, Some(_))) + .collect::>>() + .unwrap(); // should not panic cause already filtering out the None values // updating variants with context and override ids for i in 0..created_contexts.len() { @@ -249,7 +263,7 @@ pub async fn conclude( if variant.id == winner_variant_id { let context_put_req = ContextPutReq { context: experiment_context.clone(), - r#override: variant.overrides, + r#override: json!(variant.overrides), }; is_valid_winner_variant = true; @@ -402,8 +416,7 @@ async fn ramp( let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); - use crate::db::schema::cac_v1::experiments::dsl::*; - let experiment: Experiment = experiments + let experiment: Experiment = experiments::experiments .find(exp_id) .get_result::(&mut conn)?; @@ -431,13 +444,13 @@ async fn ramp( possible_fix: "".to_string(), })); } - let new_traffic_percentage = diesel::update(experiments) - .filter(id.eq(exp_id)) + let new_traffic_percentage = diesel::update(experiments::experiments) + .filter(experiments::id.eq(exp_id)) .set(( - traffic_percentage.eq(req.traffic_percentage as i32), - last_modified.eq(Utc::now()), - last_modified_by.eq(user.email), - status.eq(ExperimentStatusType::INPROGRESS), + experiments::traffic_percentage.eq(req.traffic_percentage as i32), + experiments::last_modified.eq(Utc::now()), + experiments::last_modified_by.eq(user.email), + experiments::status.eq(ExperimentStatusType::INPROGRESS), )) .execute(&mut conn)?; @@ -451,3 +464,190 @@ async fn ramp( exp_id ))); } + +#[put("/{id}/override_keys")] +async fn update_override_keys( + params: web::Path, + state: Data, + db_conn: DbConnection, + user: User, + req: web::Json, +) -> app::Result> { + let DbConnection(mut conn) = db_conn; + let experiment_id = params.into_inner(); + + let payload = req.into_inner(); + let override_keys = payload.override_keys; + let variants = payload.variants; + + // fetch the current variants of the experiment + let experiment = experiments::experiments + .find(experiment_id) + .first::(&mut conn)?; + + if experiment.status != ExperimentStatusType::CREATED { + return Err(err::BadRequest(ErrorResponse { + message: "Only experiments in CREATED state can be updated".to_string(), + possible_fix: "Please refer the documentation or contact an admin" + .to_string(), + })); + } + + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|e| { + err::InternalServerErr( + format!("Failed to parse exisitng variants: {e}").to_string(), + ) + })?; + + let id_to_existing_variant: HashMap = HashMap::from_iter( + experiment_variants + .iter() + .map(|variant| ((*variant).id.to_string(), variant)) + .collect::>(), + ); + + /****************** Validating override_keys and variant overrides *********************/ + + // checking if variants passed with correct existing variant ids + let variant_ids: HashSet = HashSet::from_iter( + variants + .iter() + .map(|variant| (*variant).id.to_string()) + .collect::>(), + ); + for existing_id in id_to_existing_variant.keys() { + if !variant_ids.contains(existing_id) { + return Err(err::BadRequest(ErrorResponse { + message: + "some variant ids do not match with exisiting experiment variants" + .to_string(), + possible_fix: "provide all existing variants of the experiment" + .to_string(), + })); + } + } + + // Checking if all the variants are overriding the mentioned keys + let mut new_variants: Vec = variants + .into_iter() + .map(|variant| { + let existing_variant: &Variant = + id_to_existing_variant.get(&variant.id).unwrap(); + Variant { + id: variant.id, + variant_type: existing_variant.variant_type.clone(), + overrides: variant.overrides, + override_id: None, + context_id: None, + } + }) + .collect(); + + let variant_overrides = new_variants.iter().map(|variant| variant.overrides.clone()).collect::>>(); + let are_valid_variants = + check_variants_override_coverage(&variant_overrides, &override_keys); + if !are_valid_variants { + return Err(err::BadRequest(ErrorResponse { + message: "all variants should contain the keys mentioned in override_keys" + .to_string(), + possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", override_keys.join(",")) + })); + } + + // validating experiment against other active experiments based on permission flags + let flags = &state.experimentation_flags; + let (valid, reason) = validate_experiment( + &experiment.context, + &override_keys, + Some(experiment_id), + &flags, + &mut conn, + )?; + if !valid { + return Err(err::BadRequest(ErrorResponse { + message: reason, + possible_fix: "Please refer documentation or contact an admin".to_string(), + })); + } + + /******************************* Updating contexts ************************************/ + let mut cac_operations: Vec = vec![]; + + // adding operations to remove exisiting variant contexts + for existing_variant in experiment_variants { + let context_id = existing_variant + .context_id + .ok_or(format!( + "Context Id not available for variant {:?}", + existing_variant.id + )) + .map_err(|e| err::InternalServerErr(e))?; + cac_operations.push(ContextAction::DELETE(context_id.to_string())); + } + + // adding operations to create new updated variant contexts + for variant in &mut new_variants { + let updated_cacccontext = + add_variant_dimension_to_ctx(&experiment.context, variant.id.to_string()) + .map_err(|e| { + err::InternalServerErr( + format!("failed to add variant dimension to context: {e}") + .to_string(), + ) + })?; + + let payload = ContextPutReq { + context: updated_cacccontext + .as_object() + .ok_or(err::InternalServerErr( + "failed to parse updated context with variant dimension".to_string(), + ))? + .clone(), + r#override: json!(variant.overrides), + }; + cac_operations.push(ContextAction::PUT(payload)); + } + + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + + let created_contexts: Vec = http_client + .put(&url) + .bearer_auth(&state.admin_token) + .json(&cac_operations) + .send() + .await + .map_err(|e| err::InternalServerErr(e.to_string()))? + .json::>() + .await + .map_err(|e| err::InternalServerErr(e.to_string()))? + .into_iter() + .map(|response| match response { + ContextBulkResponse::PUT(created_context) => Some(created_context), + _ => None, + }) + .filter(|response| matches!(response, Some(_))) + .collect::>>() + .unwrap(); // should not panic cause already filtering out the None values + + /*************************** Updating experiment in DB **************************/ + + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; + + new_variants[i].context_id = Some(created_context.context_id.clone()); + new_variants[i].override_id = Some(created_context.override_id.clone()); + } + + let updated_experiment = diesel::update(experiments::experiments.find(experiment_id)) + .set(( + experiments::variants.eq(serde_json::to_value(new_variants).unwrap()), + experiments::override_keys.eq(override_keys), + experiments::last_modified.eq(Utc::now()), + experiments::last_modified_by.eq(user.email), + )) + .get_result::(&mut conn)?; + + return Ok(Json(ExperimentResponse::from(updated_experiment))); +} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index d45d90da1..7ed792e24 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -1,4 +1,4 @@ -use super::types::{ExperimentCreateRequest, Variant, VariantType}; +use super::types::{Variant, VariantType}; use crate::db::models::{Experiment, ExperimentStatusType}; use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; @@ -153,23 +153,31 @@ pub fn are_overlapping_contexts( Ok(is_overlapping) } + +pub fn check_variant_override_coverage( + variant_override: &Map, + override_keys: &Vec +) -> bool { + if variant_override.keys().len() != override_keys.len() { + return false; + } + + for override_key in override_keys { + if variant_override.get(override_key).is_none() { + return false; + } + } + return true; +} + pub fn check_variants_override_coverage( - variants: &Vec, + variant_overrides: &Vec>, override_keys: &Vec, ) -> bool { let mut has_complete_coverage = true; - for variant in variants { - let overrides = &variant.overrides; - let mut is_valid_variant = true; - - for override_key in override_keys { - let has_override_key = match overrides[override_key] { - Value::Null => false, - _ => true, - }; - is_valid_variant = is_valid_variant && has_override_key; - } + for variant_override in variant_overrides { + let is_valid_variant = check_variant_override_coverage(&variant_override, override_keys); has_complete_coverage = has_complete_coverage && is_valid_variant; if !has_complete_coverage { @@ -180,55 +188,52 @@ pub fn check_variants_override_coverage( has_complete_coverage } -pub fn validate_experiment( - experiment: &ExperimentCreateRequest, +pub fn is_valid_experiment( + context: &Value, + override_keys: &Vec, flags: &ExperimentationFlags, - conn: &mut PgConnection, + active_experiments: &Vec ) -> app::Result<(bool, String)> { - use crate::db::schema::cac_v1::experiments::dsl::*; - - let created_perdicate = status.eq(ExperimentStatusType::CREATED); - let inprogress_predicate = status.eq(ExperimentStatusType::INPROGRESS); - let active_experiments_filter = - experiments.filter(created_perdicate.or(inprogress_predicate)); - - let active_experiments: Vec = active_experiments_filter.load(conn)?; - let mut valid_experiment = true; let mut invalid_reason = String::new(); if !flags.allow_same_keys_overlapping_ctx || !flags.allow_diff_keys_overlapping_ctx || !flags.allow_same_keys_non_overlapping_ctx { - let override_keys_set: HashSet<_> = experiment.override_keys.iter().collect(); + let override_keys_set: HashSet<_> = override_keys.iter().collect(); for active_experiment in active_experiments.iter() { let are_overlapping = - are_overlapping_contexts(&experiment.context, &active_experiment.context) - .map_err(|e| { - log::info!("validate_experiment: {e}"); - err::BadArgument(ErrorResponse { - message: "Failed to validate for overlapping context. One of the current running experiments already has this context or overlaps with it".into(), - possible_fix: "Overlapping contexts are not allowed currently as per your configuration of CAC".into(), - }) - })?; - - let have_intersecting_key_set = active_experiment - .override_keys - .iter() - .any(|key| override_keys_set.contains(key)); - - if !flags.allow_diff_keys_overlapping_ctx { - valid_experiment = - valid_experiment && !(are_overlapping && !have_intersecting_key_set); - } - if !flags.allow_same_keys_overlapping_ctx { - valid_experiment = - valid_experiment && !(are_overlapping && have_intersecting_key_set); - } - if !flags.allow_same_keys_non_overlapping_ctx { - valid_experiment = - valid_experiment && !(!are_overlapping && have_intersecting_key_set); - } + are_overlapping_contexts(context, &active_experiment.context) + .map_err(|e| { + log::info!("validate_experiment: {e}"); + err::BadArgument(ErrorResponse { + message: "Failed to validate for overlapping context. One of the current running experiments already has this context or overlaps with it".into(), + possible_fix: "Overlapping contexts are not allowed currently as per your configuration of CAC".into(), + }) + })?; + + let have_intersecting_key_set = active_experiment + .override_keys + .iter() + .any(|key| override_keys_set.contains(key)); + + let same_key_set = active_experiment + .override_keys + .iter() + .all(|key| override_keys_set.contains(key)); + + if !flags.allow_diff_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && !same_key_set); + } + if !flags.allow_same_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && have_intersecting_key_set); + } + if !flags.allow_same_keys_non_overlapping_ctx { + valid_experiment = + valid_experiment && !(!are_overlapping && have_intersecting_key_set); + } if !valid_experiment { invalid_reason.push_str("This current context overlaps with an existing experiment or the keys in the context are overlapping"); @@ -240,6 +245,29 @@ pub fn validate_experiment( Ok((valid_experiment, invalid_reason)) } +pub fn validate_experiment( + context: &Value, + override_keys: &Vec, + experiment_id: Option, + flags: &ExperimentationFlags, + conn: &mut PgConnection, +) -> app::Result<(bool, String)> { + use crate::db::schema::cac_v1::experiments::dsl as experiments_dsl; + + let active_experiments: Vec = experiments_dsl::experiments + .filter( + diesel::dsl::not(experiments_dsl::id.eq(experiment_id.unwrap_or_default())) + .and( + experiments_dsl::status + .eq(ExperimentStatusType::CREATED) + .or(experiments_dsl::status.eq(ExperimentStatusType::INPROGRESS)), + ), + ) + .load(conn)?; + + is_valid_experiment(context, override_keys, flags, &active_experiments) +} + pub fn add_variant_dimension_to_ctx( context_json: &Value, variant: String, @@ -282,4 +310,4 @@ pub fn add_variant_dimension_to_ctx( possible_fix: "Check the request sent for correctness".to_string(), })), } -} +} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 4f4e6f40d..72dbaca6b 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,8 +1,9 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Value, Map}; use service_utils::helpers::deserialize_stringified_list; + use crate::db::models::{self, ExperimentStatusType}; #[derive(Deserialize, Serialize, Clone, PartialEq, Debug)] @@ -17,7 +18,7 @@ pub struct Variant { pub variant_type: VariantType, pub context_id: Option, pub override_id: Option, - pub overrides: Value, + pub overrides: Map, } /********** Experiment Create Req Types ************/ @@ -121,6 +122,13 @@ pub struct ContextPutResp { pub priority: i32, } +#[derive(Serialize, Deserialize)] +pub enum ContextBulkResponse { + PUT(ContextPutResp), + DELETE(String), + MOVE(ContextPutResp), +} + /********** List API Filter Type *************/ #[derive(Deserialize, Debug, Clone)] @@ -138,7 +146,22 @@ pub struct ListFilters { pub count: Option, } +/********** Ramp API type **********/ #[derive(Deserialize, Debug)] pub struct RampRequest { pub traffic_percentage: u64, } + +/********** Update API type ********/ + +#[derive(Deserialize, Debug)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: Map, +} + +#[derive(Deserialize, Debug)] +pub struct OverrideKeysUpdateRequest { + pub override_keys: Vec, + pub variants: Vec, +} \ No newline at end of file From 7a60eead9bba18b7b13cf0815410999cd68aed0d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 4 Sep 2023 20:34:16 +0530 Subject: [PATCH 147/352] test: added tests for experiment helper fnxs --- .../tests/experimentation_tests.rs | 563 ++++++++++++++++++ makefile | 1 + 2 files changed, 564 insertions(+) create mode 100644 crates/experimentation-platform/tests/experimentation_tests.rs diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs new file mode 100644 index 000000000..2cc372816 --- /dev/null +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -0,0 +1,563 @@ +use chrono::Utc; +use experimentation_platform::api::experiments::helpers; +use experimentation_platform::db::models::{Experiment, ExperimentStatusType}; +use serde_json::{json, Map, Value}; +use service_utils::errors::types::Error as AppError; +use service_utils::service::types::ExperimentationFlags; + +enum Dimensions { + OS(String), + CLIENT(String), +} + +fn single_dimension_ctx_gen(value: Dimensions) -> serde_json::Value { + match value { + Dimensions::OS(os) => serde_json::json!({ + "==": [ + {"var": "os"}, + os + ] + }), + Dimensions::CLIENT(client_id) => serde_json::json!({ + "==": [ + {"var": "clientId"}, + client_id + ] + }), + } +} + +fn multiple_dimension_ctx_gen(values: Vec) -> serde_json::Value { + let mut conditions: Vec = vec![]; + for val in values { + conditions.push(single_dimension_ctx_gen(val)); + } + + serde_json::json!({ "and": conditions }) +} + +fn experiment_gen( + override_keys: &Vec, + context: &Value, + status: ExperimentStatusType, + variants: &Value, +) -> Experiment { + Experiment { + id: 123456789, + created_at: Utc::now(), + created_by: "test".to_string(), + last_modified: Utc::now(), + last_modified_by: "test".to_string(), + name: "experiment-test".to_string(), + traffic_percentage: 0, + + override_keys: override_keys.to_vec(), + status: status, + context: context.clone(), + variants: variants.clone(), + chosen_variant: None, + } +} + +#[test] +fn test_extract_dimensions() -> Result<(), AppError> { + let context_a = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let context_b = + single_dimension_ctx_gen(Dimensions::CLIENT("testclient1".to_string())); + + let expected_dimensions_1 = serde_json::Map::from_iter(vec![ + ("os".to_string(), json!("os1")), + ("clientId".to_string(), json!("testclient1")), + ]); + let expected_dimensions_2 = + serde_json::Map::from_iter(vec![("clientId".to_string(), json!("testclient1"))]); + + // more than one dimension in context + assert_eq!( + helpers::extract_dimensions(&context_a)?, + expected_dimensions_1 + ); + // only one dimension in context + assert_eq!( + helpers::extract_dimensions(&context_b)?, + expected_dimensions_2 + ); + Ok(()) +} + +#[test] +fn test_are_overlapping_contexts() -> Result<(), AppError> { + let context_a = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let context_b = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]); + let context_c = single_dimension_ctx_gen(Dimensions::OS("os1".to_string())); + let context_d = single_dimension_ctx_gen(Dimensions::OS("os2".to_string())); + + // both contexts with same dimensions + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_a)?, + true + ); + // contexts with one different dimension + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_b)?, + false + ); + // one context dimensions are subset of other + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_c)?, + true + ); + // one context dimensions not a subset of other but have less dimensions that other + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_d)?, + false + ); + // disjoint contexts + assert_eq!( + helpers::are_overlapping_contexts(&context_c, &context_d)?, + false + ); + Ok(()) +} + +#[test] +fn test_check_variants_override_coverage() { + let override_keys = vec!["key1".to_string(), "key2".to_string()]; + let overrides = vec![ + // has all mentioned override keys + Map::from_iter(vec![ + ("key1".to_string(), json!("value1")), + ("key2".to_string(), json!("value2")), + ]), + // has one override key mi)ssing + Map::from_iter(vec![("key1".to_string(), json!("value1"))]), + // has an unknown override) key + Map::from_iter(vec![("key3".to_string(), json!("value3"))]), + // has an extra unknown ov)erride key + Map::from_iter(vec![ + ("key1".to_string(), json!("value1")), + ("key2".to_string(), json!("value2")), + ("key3".to_string(), json!("value3")), + ]), + ]; + + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[0], &override_keys), + true + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[1], &override_keys), + false + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[2], &override_keys), + false + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[3], &override_keys), + false + ); +} + +/************************* No Restrictions *****************************************/ + +#[test] +fn test_is_valid_experiment_no_restrictions_overlapping_experiment( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key2".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_no_restrictions_non_overlapping_experiment( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key2".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Same Keys Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_one_same_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Different Keys Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_one_diff_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Same Keys Non Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: false, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_one_diff_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: false, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} \ No newline at end of file diff --git a/makefile b/makefile index bbd48dfe0..a8e3cfd59 100644 --- a/makefile +++ b/makefile @@ -52,6 +52,7 @@ ci-test: -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") npm ci --loglevel=error + cargo test make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ do echo "ci-test: waiting for bootup..." && sleep 4; \ From 38bc0e923533748e9d03d013bd1963073e5cb97d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 4 Sep 2023 21:30:52 +0530 Subject: [PATCH 148/352] test: added postman test for update override_keys api --- ...mentation-platform.postman_collection.json | 631 ++++++++++++++++++ postman/experimentation-platform/.meta.json | 2 + .../Create Experiment 2/.event.meta.json | 6 + .../Create Experiment 2/event.prerequest.js | 77 +++ .../Create Experiment 2/event.test.js | 219 ++++++ .../Create Experiment 2/request.json | 73 ++ .../Create Experiment 2/response.json | 1 + .../Update Override Keys/.event.meta.json | 6 + .../Update Override Keys/event.prerequest.js | 41 ++ .../Update Override Keys/event.test.js | 192 ++++++ .../Update Override Keys/request.json | 51 ++ .../Update Override Keys/response.json | 1 + 12 files changed, 1300 insertions(+) create mode 100644 postman/experimentation-platform/Create Experiment 2/.event.meta.json create mode 100644 postman/experimentation-platform/Create Experiment 2/event.prerequest.js create mode 100644 postman/experimentation-platform/Create Experiment 2/event.test.js create mode 100644 postman/experimentation-platform/Create Experiment 2/request.json create mode 100644 postman/experimentation-platform/Create Experiment 2/response.json create mode 100644 postman/experimentation-platform/Update Override Keys/.event.meta.json create mode 100644 postman/experimentation-platform/Update Override Keys/event.prerequest.js create mode 100644 postman/experimentation-platform/Update Override Keys/event.test.js create mode 100644 postman/experimentation-platform/Update Override Keys/request.json create mode 100644 postman/experimentation-platform/Update Override Keys/response.json diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json index ffd8a3a8f..d9be9d07c 100644 --- a/postman/experimentation-platform.postman_collection.json +++ b/postman/experimentation-platform.postman_collection.json @@ -619,6 +619,637 @@ }, "response": [] }, + { + "name": "Create Experiment 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey4`,", + " `pmTestKey3`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, type: \"STRING\"},", + " {name: \"client\", priority: 100, type: \"STRING\"},", + " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"type\": dimension.type", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_dimensions();", + "create_default_config_keys();" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value3-control\",", + " \"pmTestKey4\": \"value3-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value4-test\",", + " \"pmTestKey4\": \"value4-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"experiment-1\",\n \"override_keys\": [\"pmTestKey3\", \"pmTestKey4\"],\n \"traffic_percentage\": 10,\n \"context\": {\n \"and\": [\n {\n \"==\": [\n {\n \"var\": \"os\"\n },\n \"ios\"\n ]\n },\n {\n \"==\": [\n {\n \"var\": \"client\"\n },\n \"testClientCac02\"\n ]\n }\n ]\n },\n \"variants\": [\n {\n \"id\": \"control\",\n \"variant_type\": \"CONTROL\",\n \"overrides\": {\n \"pmTestKey3\": \"value3-control\",\n \"pmTestKey4\": \"value3-control\"\n }\n },\n {\n \"id\": \"test1\",\n \"variant_type\": \"EXPERIMENTAL\",\n \"overrides\": {\n \"pmTestKey3\": \"value4-test\",\n \"pmTestKey4\": \"value4-test\"\n }\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Update Override Keys", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey790`,", + " `pmTestKey690`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "create_default_config_keys()" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test updated experiment\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.id;", + "", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7910-an-control\",", + " \"pmTestKey1999\": \"value-6910-an-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7920-an-test\",", + " \"pmTestKey1999\": \"value-6930-an-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"override_keys\": [\"pmTestKey1972\", \"pmTestKey1999\"],\n \"variants\": [\n {\n \"id\": \"{{experiment_id}}-control\",\n \"overrides\": {\n \"pmTestKey1972\": \"value-7910-an-control\",\n \"pmTestKey1999\": \"value-6910-an-control\"\n }\n },\n {\n \"id\": \"{{experiment_id}}-test1\",\n \"overrides\": {\n \"pmTestKey1972\": \"value-7920-an-test\",\n \"pmTestKey1999\": \"value-6930-an-test\"\n }\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/override_keys", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "override_keys" + ] + } + }, + "response": [] + }, { "name": "List experiments [No If-Modified-Since]", "request": { diff --git a/postman/experimentation-platform/.meta.json b/postman/experimentation-platform/.meta.json index f6742bd22..77d3316e1 100644 --- a/postman/experimentation-platform/.meta.json +++ b/postman/experimentation-platform/.meta.json @@ -4,6 +4,8 @@ "Get Experiment", "Ramp", "Conclude", + "Create Experiment 2", + "Update Override Keys", "List experiments [No If-Modified-Since]", "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]" diff --git a/postman/experimentation-platform/Create Experiment 2/.event.meta.json b/postman/experimentation-platform/Create Experiment 2/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js new file mode 100644 index 000000000..8e3c7eb64 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js @@ -0,0 +1,77 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey4`, + `pmTestKey3` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +function create_dimensions() { + const dimensions = [ + {name: "os", priority: 10, type: "STRING"}, + {name: "client", priority: 100, type: "STRING"}, + {name: "variantIds", priority: 1000, type: "STRING"} + ]; + + for (const dimension of dimensions) { + const options = { + 'method': 'PUT', + 'url': `${host}/dimension`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "dimension": dimension.name, + "priority": dimension.priority, + "type": dimension.type + }) + } + }; + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating dimension: ${dimension.name}`); + console.log(error); + return; + } + console.log(`Created dimension: ${dimension.name}`); + }); + } +} + +create_dimensions(); +create_default_config_keys(); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/event.test.js b/postman/experimentation-platform/Create Experiment 2/event.test.js new file mode 100644 index 000000000..e8337b7e7 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/event.test.js @@ -0,0 +1,219 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const context = experiment.context; + console.log("Testing Context of Experiment"); + console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`); + console.log(`Actual: ${JSON.stringify(context, null, 2)}`); + pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context)); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test created contexts", function() { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + + const expected_context = { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + } + ] + }; + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey3": "value3-control", + "pmTestKey4": "value3-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey3": "value4-test", + "pmTestKey4": "value4-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/request.json b/postman/experimentation-platform/Create Experiment 2/request.json new file mode 100644 index 000000000..889c65094 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/request.json @@ -0,0 +1,73 @@ +{ + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "name": "experiment-1", + "override_keys": [ + "pmTestKey3", + "pmTestKey4" + ], + "traffic_percentage": 10, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + } + ] + }, + "variants": [ + { + "id": "control", + "variant_type": "CONTROL", + "overrides": { + "pmTestKey3": "value3-control", + "pmTestKey4": "value3-control" + } + }, + { + "id": "test1", + "variant_type": "EXPERIMENTAL", + "overrides": { + "pmTestKey3": "value4-test", + "pmTestKey4": "value4-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } +} diff --git a/postman/experimentation-platform/Create Experiment 2/response.json b/postman/experimentation-platform/Create Experiment 2/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Update Override Keys/.event.meta.json b/postman/experimentation-platform/Update Override Keys/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Update Override Keys/event.prerequest.js b/postman/experimentation-platform/Update Override Keys/event.prerequest.js new file mode 100644 index 000000000..748d5940e --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/event.prerequest.js @@ -0,0 +1,41 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey790`, + `pmTestKey690` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +create_default_config_keys() \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/event.test.js b/postman/experimentation-platform/Update Override Keys/event.test.js new file mode 100644 index 000000000..c55a11d1d --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/event.test.js @@ -0,0 +1,192 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test updated experiment", function() { + const response = pm.response.json(); + const experiment_id = response.id; + + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey1972": "value-7910-an-control", + "pmTestKey1999": "value-6910-an-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey1972": "value-7920-an-test", + "pmTestKey1999": "value-6930-an-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/request.json b/postman/experimentation-platform/Update Override Keys/request.json new file mode 100644 index 000000000..5b84469b3 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/request.json @@ -0,0 +1,51 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "override_keys": [ + "pmTestKey1972", + "pmTestKey1999" + ], + "variants": [ + { + "id": "{{experiment_id}}-control", + "overrides": { + "pmTestKey1972": "value-7910-an-control", + "pmTestKey1999": "value-6910-an-control" + } + }, + { + "id": "{{experiment_id}}-test1", + "overrides": { + "pmTestKey1972": "value-7920-an-test", + "pmTestKey1999": "value-6930-an-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/override_keys", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "override_keys" + ] + } +} diff --git a/postman/experimentation-platform/Update Override Keys/response.json b/postman/experimentation-platform/Update Override Keys/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/response.json @@ -0,0 +1 @@ +[] From 400f3f08467328438aedb497718aea5b4f2ee80a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Tue, 12 Sep 2023 15:31:03 +0530 Subject: [PATCH 149/352] fix: validating override_keys for unique entries --- .../src/api/experiments/handlers.rs | 4 +++ .../src/api/experiments/helpers.rs | 25 +++++++++++++------ .../tests/experimentation_tests.rs | 12 +++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index b406c9292..099d6f6b1 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -26,6 +26,7 @@ use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, check_variants_override_coverage, validate_experiment, + validate_override_keys }, types::{ ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextPutReq, @@ -79,6 +80,7 @@ async fn create( // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant + validate_override_keys(&override_keys)?; check_variant_types(&variants)?; // Checking if all the variants are overriding the mentioned keys @@ -509,6 +511,8 @@ async fn update_override_keys( /****************** Validating override_keys and variant overrides *********************/ + validate_override_keys(&override_keys)?; + // checking if variants passed with correct existing variant ids let variant_ids: HashSet = HashSet::from_iter( variants diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 7ed792e24..2bda559f1 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -38,6 +38,20 @@ pub fn check_variant_types(variants: &Vec) -> app::Result<()> { Ok(()) } +pub fn validate_override_keys(override_keys: &Vec) -> app::Result<()> { + let mut key_set: HashSet<&str> = HashSet::new(); + for key in override_keys { + if !key_set.insert(key) { + return Err(err::BadArgument(ErrorResponse { + message: "override_keys are not unique".to_string(), + possible_fix: "remove duplicate entries in override_keys".to_string() + })); + } + } + + Ok(()) +} + pub fn extract_dimensions(context_json: &Value) -> app::Result> { // Assuming max 2-level nesting in context json logic let context = context_json @@ -174,18 +188,13 @@ pub fn check_variants_override_coverage( variant_overrides: &Vec>, override_keys: &Vec, ) -> bool { - let mut has_complete_coverage = true; - for variant_override in variant_overrides { - let is_valid_variant = check_variant_override_coverage(&variant_override, override_keys); - - has_complete_coverage = has_complete_coverage && is_valid_variant; - if !has_complete_coverage { - break; + if !check_variant_override_coverage(&variant_override, override_keys) { + return false; } } - has_complete_coverage + return true; } pub fn is_valid_experiment( diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs index 2cc372816..a17e3223a 100644 --- a/crates/experimentation-platform/tests/experimentation_tests.rs +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -59,6 +59,18 @@ fn experiment_gen( } } +#[test] +fn test_duplicate_override_key_entries() { + let override_keys = vec!["key1".to_string(), "key2".to_string(), "key1".to_string()]; + assert!(matches!(helpers::validate_override_keys(&override_keys), Err(AppError::BadArgument(_)))); +} + +#[test] +fn test_unique_override_key_entries() { + let override_keys = vec!["key1".to_string(), "key2".to_string()]; + assert!(matches!(helpers::validate_override_keys(&override_keys), Ok(()))); +} + #[test] fn test_extract_dimensions() -> Result<(), AppError> { let context_a = multiple_dimension_ctx_gen(vec![ From 4422c83048324ea34a81ed7973fa04a2e6114e58 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 14 Sep 2023 16:44:47 +0530 Subject: [PATCH 150/352] refactor: resolved comments --- .../src/api/context/handlers.rs | 2 +- .../src/api/experiments/handlers.rs | 53 +++++++++++-------- .../src/api/experiments/helpers.rs | 4 +- .../src/api/experiments/types.rs | 4 +- ...mentation-platform.postman_collection.json | 4 +- .../Update Override Keys/request.json | 6 +-- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index e97418646..90cc9700e 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -23,7 +23,7 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::{json, Value, Value::Null}; +use serde_json::{Value, Value::Null}; use service_utils::{helpers::ToActixErr, service::types::AppState}; pub fn endpoints() -> Scope { diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 099d6f6b1..716a6bc07 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -51,7 +51,7 @@ pub fn endpoints(scope : Scope) -> Scope { .service(list_experiments) .service(get_experiment_handler) .service(ramp) - .service(update_override_keys) + .service(update_overrides) } #[post("")] @@ -84,7 +84,7 @@ async fn create( check_variant_types(&variants)?; // Checking if all the variants are overriding the mentioned keys - let variant_overrides = variants.iter().map(|variant| variant.overrides.clone()).collect::>>(); + let variant_overrides = variants.iter().map(|variant| &variant.overrides).collect::>>(); let are_valid_variants = check_variants_override_coverage(&variant_overrides, override_keys); if !are_valid_variants { return Err(err::BadRequest(ErrorResponse { @@ -156,13 +156,14 @@ async fn create( .await .map_err(|e| err::InternalServerErr(e.to_string()))? .into_iter() - .map(|response| match response { - ContextBulkResponse::PUT(created_context) => Some(created_context), - _ => None, - }) - .filter(|response| matches!(response, Some(_))) - .collect::>>() - .unwrap(); // should not panic cause already filtering out the None values + .fold(Vec::::new(), |mut put_responses, response| { + if let ContextBulkResponse::PUT(created_context) = response { + put_responses.push(created_context); + } else { + log::error!("unexpected response from cac for create only request: {:?}", response); + } + put_responses + }); // updating variants with context and override ids for i in 0..created_contexts.len() { @@ -265,7 +266,7 @@ pub async fn conclude( if variant.id == winner_variant_id { let context_put_req = ContextPutReq { context: experiment_context.clone(), - r#override: json!(variant.overrides), + r#override: serde_json::Value::Object(variant.overrides), }; is_valid_winner_variant = true; @@ -467,13 +468,14 @@ async fn ramp( ))); } -#[put("/{id}/override_keys")] -async fn update_override_keys( +#[put("/{id}/overrides")] +async fn update_overrides( params: web::Path, state: Data, db_conn: DbConnection, user: User, req: web::Json, + tenant: Tenant ) -> app::Result> { let DbConnection(mut conn) = db_conn; let experiment_id = params.into_inner(); @@ -505,7 +507,7 @@ async fn update_override_keys( let id_to_existing_variant: HashMap = HashMap::from_iter( experiment_variants .iter() - .map(|variant| ((*variant).id.to_string(), variant)) + .map(|variant| (variant.id.to_string(), variant)) .collect::>(), ); @@ -548,7 +550,7 @@ async fn update_override_keys( }) .collect(); - let variant_overrides = new_variants.iter().map(|variant| variant.overrides.clone()).collect::>>(); + let variant_overrides = new_variants.iter().map(|variant| &variant.overrides).collect::>>(); let are_valid_variants = check_variants_override_coverage(&variant_overrides, &override_keys); if !are_valid_variants { @@ -618,7 +620,8 @@ async fn update_override_keys( let created_contexts: Vec = http_client .put(&url) - .bearer_auth(&state.admin_token) + .header("Authorization", format!("Bearer {}", user.token)) + .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() .await @@ -627,13 +630,12 @@ async fn update_override_keys( .await .map_err(|e| err::InternalServerErr(e.to_string()))? .into_iter() - .map(|response| match response { - ContextBulkResponse::PUT(created_context) => Some(created_context), - _ => None, - }) - .filter(|response| matches!(response, Some(_))) - .collect::>>() - .unwrap(); // should not panic cause already filtering out the None values + .fold(Vec::::new(), |mut put_responses, response| { + if let ContextBulkResponse::PUT(created_context) = response { + put_responses.push(created_context); + } + put_responses + }); /*************************** Updating experiment in DB **************************/ @@ -644,9 +646,14 @@ async fn update_override_keys( new_variants[i].override_id = Some(created_context.override_id.clone()); } + let new_variants_json = serde_json::to_value(new_variants).map_err(|e| { + err::InternalServerErr( + format!("failed to convert new variants to json: {e}") + ) + })?; let updated_experiment = diesel::update(experiments::experiments.find(experiment_id)) .set(( - experiments::variants.eq(serde_json::to_value(new_variants).unwrap()), + experiments::variants.eq(new_variants_json), experiments::override_keys.eq(override_keys), experiments::last_modified.eq(Utc::now()), experiments::last_modified_by.eq(user.email), diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 2bda559f1..d3c7afd99 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -185,11 +185,11 @@ pub fn check_variant_override_coverage( } pub fn check_variants_override_coverage( - variant_overrides: &Vec>, + variant_overrides: &Vec<&Map>, override_keys: &Vec, ) -> bool { for variant_override in variant_overrides { - if !check_variant_override_coverage(&variant_override, override_keys) { + if !check_variant_override_coverage(variant_override, override_keys) { return false; } } diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 72dbaca6b..24254ca01 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -115,14 +115,14 @@ pub enum ContextAction { MOVE((String, ContextPutReq)), } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct ContextPutResp { pub context_id: String, pub override_id: String, pub priority: i32, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub enum ContextBulkResponse { PUT(ContextPutResp), DELETE(String), diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json index d9be9d07c..9e092edfa 100644 --- a/postman/experimentation-platform.postman_collection.json +++ b/postman/experimentation-platform.postman_collection.json @@ -1237,14 +1237,14 @@ } }, "url": { - "raw": "{{host}}/experiments/{{experiment_id}}/override_keys", + "raw": "{{host}}/experiments/{{experiment_id}}/overrides", "host": [ "{{host}}" ], "path": [ "experiments", "{{experiment_id}}", - "override_keys" + "overrides" ] } }, diff --git a/postman/experimentation-platform/Update Override Keys/request.json b/postman/experimentation-platform/Update Override Keys/request.json index 5b84469b3..502913dbd 100644 --- a/postman/experimentation-platform/Update Override Keys/request.json +++ b/postman/experimentation-platform/Update Override Keys/request.json @@ -38,14 +38,14 @@ } }, "url": { - "raw": "{{host}}/experiments/{{experiment_id}}/override_keys", + "raw": "{{host}}/experiments/{{experiment_id}}/overrides", "host": [ "{{host}}" ], "path": [ "experiments", "{{experiment_id}}", - "override_keys" + "overrides" ] } -} +} \ No newline at end of file From dc2db17bc6c03fbc91243cf7b9ec3c9a6f64ceff Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 10 Oct 2023 17:03:13 +0000 Subject: [PATCH 151/352] chore(version): v0.9.0 [skip ci] --- CHANGELOG.md | 13 +++++++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 8 ++++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 12 ++++++++++++ crates/experimentation-platform/Cargo.toml | 2 +- 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3007b40e3..d1a871288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.9.0 - 2023-10-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.8.0 +- experimentation-platform bumped to experimentation-platform-v0.5.0 +### Global changes +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev +#### Tests +- added postman test for update override_keys api - (cc96ca1) - Shubhranshu Sanjeev +- added tests for experiment helper fnxs - (ea4db17) - Shubhranshu Sanjeev + +- - - + ## v0.8.1 - 2023-10-10 ### Package updates - context-aware-config bumped to context-aware-config-v0.7.1 diff --git a/Cargo.lock b/Cargo.lock index b9cb25c13..aadc0a96e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.7.1" +version = "0.8.0" dependencies = [ "actix", "actix-cors", @@ -1181,7 +1181,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 04016f7fc..25ea656f5 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.8.0 - 2023-10-10 +#### Features +- support to update experiment override_keys and variants - (9432bf7) - Shubhranshu Sanjeev +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.7.1 - 2023-10-10 #### Bug Fixes - PICAF-24742 add migration for changing default_configs_keys - (55f8895) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index da61217fa..06060aab5 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.7.1" +version = "0.8.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 30723663b..adef729f3 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.5.0 - 2023-10-10 +#### Bug Fixes +- validating override_keys for unique entries - (36cf523) - Shubhranshu Sanjeev +#### Features +- support to update experiment override_keys and variants - (9432bf7) - Shubhranshu Sanjeev +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev +#### Tests +- added tests for experiment helper fnxs - (ea4db17) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.4.0 - 2023-10-05 #### Features - [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 63c57fce4..3dd5dd286 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.4.0" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From ef7a37d68e11a74eec92bff1ed5cb93e92b46643 Mon Sep 17 00:00:00 2001 From: Pratik Mishra Date: Wed, 4 Oct 2023 15:48:15 +0530 Subject: [PATCH 152/352] fix: add all variants in manifest --- crates/superposition_client/src/lib.rs | 28 ++++++++++++------------ crates/superposition_client/src/types.rs | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index addda0e84..fab4a39cb 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -7,10 +7,8 @@ use tokio::{ sync::RwLock, time::{self, Duration}, }; -pub use types::{Config, Variants}; -use types::{ - ExperimentStore, Experiments, ListExperimentsResponse, Variant, VariantType, -}; +pub use types::{Config, Experiment, Experiments, Variants}; +use types::{ExperimentStore, ListExperimentsResponse, Variant, VariantType}; #[derive(Clone, Debug)] pub struct Client { @@ -69,17 +67,8 @@ impl Client { } pub async fn get_applicable_variant(&self, context: &Value, toss: i8) -> Vec { - let running_experiments = self.experiments.read().await; - // try and if json logic works - let mut experiments: Experiments = Vec::new(); - for (_, exp) in running_experiments.iter() { - if let Ok(Value::Bool(true)) = jsonlogic::apply(&exp.context, context) { - experiments.push(exp.clone()); - } - } - + let experiments: Experiments = self.get_satisfied_experiments(context).await; let mut variants: Vec = Vec::new(); - for exp in experiments { if let Some(v) = self.decide_variant(exp.traffic_percentage, exp.variants, toss) @@ -90,6 +79,17 @@ impl Client { variants } + pub async fn get_satisfied_experiments(&self, context: &Value) -> Experiments { + let running_experiments = self.experiments.read().await; + running_experiments + .iter() + .filter(|(_, exp)| { + (jsonlogic::apply(&exp.context, context) == Ok(Value::Bool(true))) + }) + .map(|(_, exp)| exp.clone()) + .collect::() + } + pub async fn get_running_experiments(&self) -> Experiments { let running_experiments = self.experiments.read().await; let experiments: Experiments = running_experiments.values().cloned().collect(); diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index ec5c231b2..f3916380a 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -33,7 +33,7 @@ pub type Variants = Vec; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Experiment { - pub(crate) variants: Variants, + pub variants: Variants, pub(crate) name: String, pub(crate) id: String, pub(crate) traffic_percentage: u8, From eddd8a40cc082fc466aaaea987f8042012c99c89 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 13 Oct 2023 09:54:53 +0000 Subject: [PATCH 153/352] chore(version): v0.9.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/superposition_client/CHANGELOG.md | 6 ++++++ crates/superposition_client/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a871288..9d113727e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.9.1 - 2023-10-13 +### Package updates +- superposition_client bumped to superposition_client-v0.1.3 +### Global changes + +- - - + ## v0.9.0 - 2023-10-10 ### Package updates - context-aware-config bumped to context-aware-config-v0.8.0 diff --git a/Cargo.lock b/Cargo.lock index aadc0a96e..d7c1c0a98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3264,7 +3264,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.1.2" +version = "0.1.3" dependencies = [ "chrono", "dotenv", diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index 41cdbe97a..7dbbdc650 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.1.3 - 2023-10-13 +#### Bug Fixes +- PICAF-24612 add all variants in manifest - (0f15ac9) - Pratik Mishra + +- - - + ## superposition_client-v0.1.2 - 2023-09-06 #### Bug Fixes - trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 6f2813243..b39acb878 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.1.2" +version = "0.1.3" edition = "2021" [dependencies] From be2ddd22498bf05da86dbbd5338a1510a6847d8a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Fri, 13 Oct 2023 13:11:03 +0530 Subject: [PATCH 154/352] validation for input override key --- Cargo.lock | 1 + crates/context-aware-config/Cargo.toml | 1 + .../src/api/context/handlers.rs | 54 +++++++++++++++++-- .../src/api/context/types.rs | 2 +- crates/context-aware-config/src/db/models.rs | 2 +- ...mentation-platform.postman_collection.json | 11 ++-- .../Update Override Keys/event.prerequest.js | 4 +- 7 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7c1c0a98..cebb77762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "actix-cors", "actix-http", "actix-web", + "anyhow", "base64 0.21.2", "blake3", "bytes 1.4.0", diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 06060aab5..1ed5c7cb7 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -56,3 +56,4 @@ futures-util = "0.3.28" external = { path = "../external"} actix-cors = "0.6.4" dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} +anyhow = { version = "1.0", default-features = false } \ No newline at end of file diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 90cc9700e..c6a26c4f1 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -4,7 +4,8 @@ use crate::{ models::{Context, Dimension}, schema::cac_v1::{ contexts::{self, id}, - dimensions::dsl::dimensions, + dimensions::dsl::dimensions, + default_configs::dsl, }, }, }; @@ -15,6 +16,7 @@ use actix_web::{ web::{self, Data, Path}, HttpResponse, Responder, Result, Scope, }; +use anyhow::anyhow; use chrono::Utc; use dashboard_auth::{middleware::acl, types::User}; use diesel::{ @@ -23,8 +25,9 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::{Value, Value::Null}; +use serde_json::{json, Value, Value::Null, Map}; use service_utils::{helpers::ToActixErr, service::types::AppState}; +use jsonschema::{Draft, JSONSchema, ValidationError}; pub fn endpoints() -> Scope { Scope::new("") @@ -69,12 +72,53 @@ fn val_dimensions_cal_priority( } } +fn validate_override_with_default_configs( + conn : &mut DBConnection, + override_ : &Map +) -> anyhow::Result<()> { + let keys_array: Vec<&String> = override_.keys().collect(); + let res: Vec<(String, Value)> = dsl::default_configs + .filter(dsl::key.eq_any(keys_array)) + .select((dsl::key, dsl::schema)) + .get_results:: <(String, Value)>(conn)?; + + let map = Map::from_iter(res); + + for (key, value) in override_.iter() { + let schema = map.get(key) + // .map(|resp| resp) + .ok_or(anyhow!(format!("failed to compile json schema for key {key}")))?; + let instance = value; + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(schema); + let jschema = match schema_compile_result { + Ok(jschema) => jschema, + Err(e) => { + log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); + return Err(anyhow!("message: bad json schema")); + } + }; + if let Err(e) = jschema.validate(instance) { + let verrors = e.collect::>(); + log::error!("{:?}", verrors); + return Err(anyhow!(json!(format!("Schema validation failed for {key}")))) + }; + } + + Ok(()) +} + fn create_ctx_from_put_req( req: web::Json, conn: &mut DBConnection, user: &User, ) -> actix_web::Result { let ctx_condition = Value::Object(req.context.to_owned()); + let ctx_override: Value = req.r#override.to_owned().into(); + validate_override_with_default_configs(conn, &req.r#override) + .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; + let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { Ok(0) => { return Err(ErrorBadRequest("No dimension found in context")); @@ -84,14 +128,14 @@ fn create_ctx_from_put_req( } Ok(p) => p, }; - let context_id = blake3::hash((ctx_condition).to_string().as_bytes()).to_string(); - let override_id = blake3::hash((req.r#override).to_string().as_bytes()).to_string(); + let context_id = blake3::hash(ctx_condition.to_string().as_bytes()).to_string(); + let override_id = blake3::hash(ctx_override.to_string().as_bytes()).to_string(); Ok(Context { id: context_id.clone(), value: ctx_condition, priority, override_id: override_id.to_owned(), - override_: req.r#override.to_owned(), + override_: ctx_override.to_owned(), created_at: Utc::now(), created_by: user.email.clone(), }) diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index b25f73cef..9e395f934 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -4,7 +4,7 @@ use serde_json::{Map, Value}; #[derive(Deserialize, Clone)] pub struct PutReq { pub context: Map, - pub r#override: Value, + pub r#override: Map, } #[derive(Serialize, Debug)] diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index ebcab9055..dc4eea716 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -35,9 +35,9 @@ pub struct Dimension { pub struct DefaultConfig { pub key: String, pub value: Value, - pub schema: Value, pub created_at: DateTime, pub created_by: String, + pub schema: Value, } #[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json index 9e092edfa..14faf2d9b 100644 --- a/postman/experimentation-platform.postman_collection.json +++ b/postman/experimentation-platform.postman_collection.json @@ -1,8 +1,9 @@ { "info": { - "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "_postman_id": "c7f42ca5-e0a2-4c26-a349-8de8c54c2cf1", "name": "experimentation-platform", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "20159828" }, "item": [ { @@ -598,7 +599,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"winner_variant\": \"{{experiment_id}}-control\"\n}", + "raw": "{\n \"chosen_variant\": \"{{experiment_id}}-control\"\n}", "options": { "raw": { "language": "json" @@ -977,8 +978,8 @@ "", "function create_default_config_keys() {", " let keys = [", - " `pmTestKey790`,", - " `pmTestKey690`", + " `pmTestKey1972`,", + " `pmTestKey1999`", " ];", "", " for (const key of keys) {", diff --git a/postman/experimentation-platform/Update Override Keys/event.prerequest.js b/postman/experimentation-platform/Update Override Keys/event.prerequest.js index 748d5940e..9b7b90d03 100644 --- a/postman/experimentation-platform/Update Override Keys/event.prerequest.js +++ b/postman/experimentation-platform/Update Override Keys/event.prerequest.js @@ -3,8 +3,8 @@ const token = pm.environment.get("token"); function create_default_config_keys() { let keys = [ - `pmTestKey790`, - `pmTestKey690` + `pmTestKey1972`, + `pmTestKey1999` ]; for (const key of keys) { From 3e9fd4af69611f152878ebe52b7ddb0c55a8cec2 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Wed, 23 Aug 2023 16:38:03 +0530 Subject: [PATCH 155/352] removed override from move context api --- .../src/api/context/handlers.rs | 111 +++++++++++------- .../src/api/context/types.rs | 7 +- .../src/api/experiments/handlers.rs | 77 ++++++------ .../src/api/experiments/types.rs | 11 +- 4 files changed, 128 insertions(+), 78 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index c6a26c4f1..e3393af30 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,11 +1,13 @@ use crate::{ - api::context::types::{PaginationParams, PutReq, PutResp, ContextAction, ContextBulkResponse}, + api::context::types::{ + ContextAction, ContextBulkResponse, MoveReq, PaginationParams, PutReq, PutResp, + }, db::{ models::{Context, Dimension}, schema::cac_v1::{ contexts::{self, id}, - dimensions::dsl::dimensions, - default_configs::dsl, + default_configs::dsl, + dimensions::dsl::dimensions, }, }, }; @@ -25,9 +27,9 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use serde_json::{json, Value, Value::Null, Map}; -use service_utils::{helpers::ToActixErr, service::types::AppState}; use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Map, Value, Value::Null}; +use service_utils::{helpers::ToActixErr, service::types::AppState}; pub fn endpoints() -> Scope { Scope::new("") @@ -73,21 +75,24 @@ fn val_dimensions_cal_priority( } fn validate_override_with_default_configs( - conn : &mut DBConnection, - override_ : &Map + conn: &mut DBConnection, + override_: &Map, ) -> anyhow::Result<()> { let keys_array: Vec<&String> = override_.keys().collect(); let res: Vec<(String, Value)> = dsl::default_configs .filter(dsl::key.eq_any(keys_array)) .select((dsl::key, dsl::schema)) - .get_results:: <(String, Value)>(conn)?; + .get_results::<(String, Value)>(conn)?; let map = Map::from_iter(res); for (key, value) in override_.iter() { - let schema = map.get(key) + let schema = map + .get(key) // .map(|resp| resp) - .ok_or(anyhow!(format!("failed to compile json schema for key {key}")))?; + .ok_or(anyhow!(format!( + "failed to compile json schema for key {key}" + )))?; let instance = value; let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) @@ -102,10 +107,13 @@ fn validate_override_with_default_configs( if let Err(e) = jschema.validate(instance) { let verrors = e.collect::>(); log::error!("{:?}", verrors); - return Err(anyhow!(json!(format!("Schema validation failed for {key}")))) + return Err(anyhow!(json!(format!( + "Schema validation failed for {key} with error {:?}", + verrors + )))); }; } - + Ok(()) } @@ -128,7 +136,7 @@ fn create_ctx_from_put_req( } Ok(p) => p, }; - let context_id = blake3::hash(ctx_condition.to_string().as_bytes()).to_string(); + let context_id = generate_context_id(&ctx_condition); let override_id = blake3::hash(ctx_override.to_string().as_bytes()).to_string(); Ok(Context { id: context_id.clone(), @@ -141,6 +149,10 @@ fn create_ctx_from_put_req( }) } +fn generate_context_id(ctx_condition: &Value) -> String { + blake3::hash(ctx_condition.to_string().as_bytes()).to_string() +} + fn update_override_of_existing_ctx( conn: &mut PgConnection, ctx: Context, @@ -235,16 +247,24 @@ async fn put_handler( fn r#move( old_ctx_id: String, - req: web::Json, + req: web::Json, user: &User, conn: &mut PooledConnection>, already_under_txn: bool, ) -> Result { use contexts::dsl; - let new_ctx = create_ctx_from_put_req(req, conn, user).map_err(|e| { - log::error!("update query failed with error: {e:?}"); - Error::STRTYPE(e.to_string()) - })?; + let req = req.into_inner(); + let ctx_condition = Value::Object(req.context); + let new_ctx_id = generate_context_id(&ctx_condition); + let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { + Ok(0) => { + return Err(Error::STRTYPE("No dimension found in context".to_string())); + } + Err(e) => { + return Err(Error::STRTYPE(e)); + } + Ok(p) => p, + }; if already_under_txn { diesel::sql_query("SAVEPOINT update_ctx_savepoint") @@ -252,41 +272,50 @@ fn r#move( .map_err(|e| Error::DIESEL(e))?; } - let update = diesel::update(dsl::contexts) + let context = diesel::update(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - .set((&new_ctx, dsl::id.eq(&new_ctx.id))) - .execute(conn); + .set((dsl::id.eq(&new_ctx_id), dsl::value.eq(&ctx_condition))) + .get_result(conn); + + let contruct_new_ctx_with_old_overrides = |ctx: Context| Context { + id: new_ctx_id, + value: ctx_condition, + priority, + created_at: Utc::now(), + created_by: user.email.clone(), + override_id: ctx.override_id, + override_: ctx.override_, + }; let handle_unique_violation = - |db_conn: &mut DBConnection, new_ctx: Context, already_under_txn: bool| { + |db_conn: &mut DBConnection, already_under_txn: bool| { if already_under_txn { - diesel::delete(dsl::contexts) + let deleted_ctxt = diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - .execute(db_conn)?; - update_override_of_existing_ctx(db_conn, new_ctx) + .get_result(db_conn)?; + + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(db_conn, ctx) } else { db_conn.build_transaction().read_write().run(|conn| { - diesel::delete(dsl::contexts) + let deleted_ctxt = diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - .execute(conn)?; - update_override_of_existing_ctx(conn, new_ctx) + .get_result(conn)?; + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(conn, ctx) }) } }; - match update { - Ok(0) => Err(Error::STRTYPE(format!( - "context with id: {old_ctx_id} not found" - ))), - Ok(_) => Ok(get_put_resp(new_ctx)), + match context { + Ok(ctx) => Ok(get_put_resp(ctx)), Err(DatabaseError(UniqueViolation, _)) => { if already_under_txn { diesel::sql_query("ROLLBACK TO update_ctx_savepoint") .execute(conn) .map_err(|e| Error::DIESEL(e))?; } - handle_unique_violation(conn, new_ctx, already_under_txn) - .map_err(|e| Error::DIESEL(e)) + handle_unique_violation(conn, already_under_txn).map_err(|e| Error::DIESEL(e)) } Err(e) => { log::error!("update query failed with error: {e:?}"); @@ -299,7 +328,7 @@ fn r#move( async fn move_handler( state: Data, path: Path, - req: web::Json, + req: web::Json, user: User, ) -> actix_web::Result> { let conn = &mut state @@ -449,7 +478,9 @@ async fn bulk_operations( Ok(0) => return Err(diesel::result::Error::RollbackTransaction), Ok(_) => { log::info!("{ctx_id} context deleted by {email}"); - resp.push(ContextBulkResponse::DELETE(format!("{ctx_id} deleted succesfully"))) + resp.push(ContextBulkResponse::DELETE(format!( + "{ctx_id} deleted succesfully" + ))) } Err(e) => { log::error!("Delete context failed due to {:?}", e); @@ -457,10 +488,10 @@ async fn bulk_operations( } }; } - ContextAction::MOVE((old_ctx_id, put_req)) => { + ContextAction::MOVE((old_ctx_id, move_req)) => { let move_context_resp = r#move( old_ctx_id, - actix_web::web::Json(put_req), + actix_web::web::Json(move_req), &user, transaction_conn, true, @@ -486,4 +517,4 @@ async fn bulk_operations( Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index 9e395f934..5a39c87fc 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -7,6 +7,11 @@ pub struct PutReq { pub r#override: Map, } +#[derive(Deserialize, Clone)] +pub struct MoveReq { + pub context: Map, +} + #[derive(Serialize, Debug)] pub struct PutResp { pub context_id: String, @@ -24,7 +29,7 @@ pub struct PaginationParams { pub enum ContextAction { PUT(PutReq), DELETE(String), - MOVE((String, PutReq)), + MOVE((String, MoveReq)), } #[derive(serde::Serialize)] diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 716a6bc07..a9bf4ae7c 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -15,7 +15,6 @@ use diesel::{ ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; - use service_utils::{ errors::types::{Error as err, ErrorResponse}, service::types::{AppState, DbConnection}, @@ -25,12 +24,11 @@ use service_utils::{ use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, - check_variants_override_coverage, validate_experiment, - validate_override_keys + check_variants_override_coverage, validate_experiment, validate_override_keys, }, types::{ - ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextPutReq, - ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, + ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextMoveReq, + ContextPutReq, ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, RampRequest, Variant, }, @@ -41,9 +39,9 @@ use crate::{ db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, }; -use serde_json::{Value, Map, json}; +use serde_json::{json, Map, Value}; -pub fn endpoints(scope : Scope) -> Scope { +pub fn endpoints(scope: Scope) -> Scope { scope .guard(acl([("mjos_manager".into(), "RW".into())])) .service(create) @@ -84,8 +82,12 @@ async fn create( check_variant_types(&variants)?; // Checking if all the variants are overriding the mentioned keys - let variant_overrides = variants.iter().map(|variant| &variant.overrides).collect::>>(); - let are_valid_variants = check_variants_override_coverage(&variant_overrides, override_keys); + let variant_overrides = variants + .iter() + .map(|variant| &variant.overrides) + .collect::>>(); + let are_valid_variants = + check_variants_override_coverage(&variant_overrides, override_keys); if !are_valid_variants { return Err(err::BadRequest(ErrorResponse { message: "all variants should contain the keys mentioned in override_keys" @@ -119,7 +121,7 @@ async fn create( //create overrides in CAC, if successfull then create experiment in DB let mut cac_operations: Vec = vec![]; - for mut variant in &mut variants { + for variant in &mut variants { let variant_id = experiment_id.to_string() + "-" + &variant.id; // updating variant.id to => experiment_id + variant.id @@ -156,14 +158,20 @@ async fn create( .await .map_err(|e| err::InternalServerErr(e.to_string()))? .into_iter() - .fold(Vec::::new(), |mut put_responses, response| { - if let ContextBulkResponse::PUT(created_context) = response { - put_responses.push(created_context); - } else { - log::error!("unexpected response from cac for create only request: {:?}", response); - } - put_responses - }); + .fold( + Vec::::new(), + |mut put_responses, response| { + if let ContextBulkResponse::PUT(created_context) = response { + put_responses.push(created_context); + } else { + log::error!( + "unexpected response from cac for create only request: {:?}", + response + ); + } + put_responses + }, + ); // updating variants with context and override ids for i in 0..created_contexts.len() { @@ -264,14 +272,13 @@ pub async fn conclude( ))?; if variant.id == winner_variant_id { - let context_put_req = ContextPutReq { + let context_move_req = ContextMoveReq { context: experiment_context.clone(), - r#override: serde_json::Value::Object(variant.overrides), }; is_valid_winner_variant = true; - operations.push(ContextAction::MOVE((context_id, context_put_req))); + operations.push(ContextAction::MOVE((context_id, context_move_req))); } else { // delete this context operations.push(ContextAction::DELETE(context_id)); @@ -475,7 +482,7 @@ async fn update_overrides( db_conn: DbConnection, user: User, req: web::Json, - tenant: Tenant + tenant: Tenant, ) -> app::Result> { let DbConnection(mut conn) = db_conn; let experiment_id = params.into_inner(); @@ -550,7 +557,10 @@ async fn update_overrides( }) .collect(); - let variant_overrides = new_variants.iter().map(|variant| &variant.overrides).collect::>>(); + let variant_overrides = new_variants + .iter() + .map(|variant| &variant.overrides) + .collect::>>(); let are_valid_variants = check_variants_override_coverage(&variant_overrides, &override_keys); if !are_valid_variants { @@ -630,12 +640,15 @@ async fn update_overrides( .await .map_err(|e| err::InternalServerErr(e.to_string()))? .into_iter() - .fold(Vec::::new(), |mut put_responses, response| { - if let ContextBulkResponse::PUT(created_context) = response { - put_responses.push(created_context); - } - put_responses - }); + .fold( + Vec::::new(), + |mut put_responses, response| { + if let ContextBulkResponse::PUT(created_context) = response { + put_responses.push(created_context); + } + put_responses + }, + ); /*************************** Updating experiment in DB **************************/ @@ -647,9 +660,7 @@ async fn update_overrides( } let new_variants_json = serde_json::to_value(new_variants).map_err(|e| { - err::InternalServerErr( - format!("failed to convert new variants to json: {e}") - ) + err::InternalServerErr(format!("failed to convert new variants to json: {e}")) })?; let updated_experiment = diesel::update(experiments::experiments.find(experiment_id)) .set(( @@ -661,4 +672,4 @@ async fn update_overrides( .get_result::(&mut conn)?; return Ok(Json(ExperimentResponse::from(updated_experiment))); -} \ No newline at end of file +} diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 24254ca01..9116d92a2 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,9 +1,8 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::{Value, Map}; +use serde_json::{Map, Value}; use service_utils::helpers::deserialize_stringified_list; - use crate::db::models::{self, ExperimentStatusType}; #[derive(Deserialize, Serialize, Clone, PartialEq, Debug)] @@ -112,7 +111,7 @@ pub struct ContextPutReq { pub enum ContextAction { PUT(ContextPutReq), DELETE(String), - MOVE((String, ContextPutReq)), + MOVE((String, ContextMoveReq)), } #[derive(Deserialize, Serialize, Debug)] @@ -164,4 +163,8 @@ pub struct VariantUpdateRequest { pub struct OverrideKeysUpdateRequest { pub override_keys: Vec, pub variants: Vec, -} \ No newline at end of file +} +#[derive(Deserialize, Serialize, Clone)] +pub struct ContextMoveReq { + pub context: serde_json::Map, +} From 2c810f7bb7eae2f7d52de67dd064c6b3e691dabc Mon Sep 17 00:00:00 2001 From: Prasanna P Date: Tue, 25 Jul 2023 19:17:54 +0530 Subject: [PATCH 156/352] feat: - Dimension value schema validation on context-addition feat: removed redundant db calls while rebasing, had to change the error type of a util fxn therefore leading to a lot of fxns having anyhow::Result return type --- .../src/api/context/handlers.rs | 146 +++++++++++------- .../src/api/context/types.rs | 7 +- .../src/api/dimension/handlers.rs | 2 +- .../src/api/dimension/mod.rs | 2 + .../src/api/dimension/utils.rs | 26 ++++ crates/context-aware-config/src/helpers.rs | 55 ++++++- crates/service-utils/src/service/types.rs | 2 + crates/superposition_client/src/lib.rs | 2 +- 8 files changed, 184 insertions(+), 58 deletions(-) create mode 100644 crates/context-aware-config/src/api/dimension/utils.rs diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index e3393af30..961fb1089 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,19 +1,24 @@ +use std::collections::HashMap; + use crate::{ - api::context::types::{ - ContextAction, ContextBulkResponse, MoveReq, PaginationParams, PutReq, PutResp, + api::{ + context::types::{ + ContextAction, ContextBulkResponse, DimensionCondition, PaginationParams, + PutReq, PutResp, MoveReq + }, + dimension::get_all_dimension_schema_map, }, db::{ - models::{Context, Dimension}, + models::Context, schema::cac_v1::{ contexts::{self, id}, default_configs::dsl, - dimensions::dsl::dimensions, }, }, }; use actix_web::{ delete, - error::{self, ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, + error::{self, ErrorInternalServerError, ErrorNotFound}, get, put, web::{self, Data, Path}, HttpResponse, Responder, Result, Scope, @@ -27,9 +32,9 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; -use jsonschema::{Draft, JSONSchema, ValidationError}; -use serde_json::{json, Map, Value, Value::Null}; +use serde_json::{from_value, json, Map, Value, Value::Null}; use service_utils::{helpers::ToActixErr, service::types::AppState}; +use jsonschema::{Draft, JSONSchema, ValidationError}; pub fn endpoints() -> Scope { Scope::new("") @@ -44,22 +49,27 @@ pub fn endpoints() -> Scope { type DBConnection = PooledConnection>; -fn val_dimensions_cal_priority( - conn: &mut DBConnection, +fn validate_dimensions_and_calculate_priority( cond: &Value, + dimension_schema_map: &HashMap, ) -> Result { - let mut get_priority = |key: &String, val: &Value| -> Result { + let get_priority = |key: &String, val: &Value| -> Result { if key == "var" { let dimension_name = val .as_str() .ok_or_else(|| "Dimension name should be of String type")?; - dimensions - .find(dimension_name) - .first(conn) - .map(|d: Dimension| d.priority) - .map_err(|e| format!("{dimension_name}: {}", e.to_string())) + dimension_schema_map + .get(dimension_name) + .map(|(_, priority)| priority) + .ok_or(String::from( + "No matching `dimension` found in dimension table", + )) + .copied() } else { - val_dimensions_cal_priority(conn, val) + validate_dimensions_and_calculate_priority( + val, + dimension_schema_map, + ) } }; @@ -67,9 +77,45 @@ fn val_dimensions_cal_priority( Value::Object(x) => x.iter().try_fold(0, |acc, (key, val)| { get_priority(key, val).map(|res| res + acc) }), - Value::Array(x) => x.iter().try_fold(0, |acc, item| { - val_dimensions_cal_priority(conn, item).map(|res| res + acc) - }), + Value::Array(arr) => { + let mut val: Option = None; + let mut condition: Option = None; + for i in arr { + if let (None, Ok(x)) = + (&condition, from_value::(json!(i))) + { + condition = Some(x); + } else if val == None { + val = Some(i.clone()); + } + + if let (Some(_dimension_value), Some(_dimension_condition)) = (&val, &condition) { + break; + } + } + + if let (Some(dimension_value), Some(dimension_condition)) = (val, condition) { + let expected_dimension_name = dimension_condition.var; + let (dimension_value_schema, _) = dimension_schema_map + .get(&expected_dimension_name) + .ok_or("No matching `dimension` for in dimension table")?; + + dimension_value_schema + .validate(&dimension_value) + .map_err(|e| { + let verrors = e.collect::>(); + String::from(format!("Bad schema: {:?}", verrors.as_slice())) + })?; + }; + + arr.iter().try_fold(0, |acc, item| { + validate_dimensions_and_calculate_priority( + item, + dimension_schema_map, + ) + .map(|res| res + acc) + }) + } _ => Ok(0), } } @@ -121,18 +167,22 @@ fn create_ctx_from_put_req( req: web::Json, conn: &mut DBConnection, user: &User, -) -> actix_web::Result { +) -> anyhow::Result { let ctx_condition = Value::Object(req.context.to_owned()); let ctx_override: Value = req.r#override.to_owned().into(); - validate_override_with_default_configs(conn, &req.r#override) - .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; + validate_override_with_default_configs(conn, &req.r#override)?; - let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { + let dimension_schema_map = get_all_dimension_schema_map(conn)?; + + let priority = match validate_dimensions_and_calculate_priority( + &ctx_condition, + &dimension_schema_map, + ) { Ok(0) => { - return Err(ErrorBadRequest("No dimension found in context")); + return Err(anyhow!("No dimension found in context")); } Err(e) => { - return Err(ErrorBadRequest(e)); + return Err(anyhow!(e)); } Ok(p) => p, }; @@ -156,7 +206,7 @@ fn generate_context_id(ctx_condition: &Value) -> String { fn update_override_of_existing_ctx( conn: &mut PgConnection, ctx: Context, -) -> Result { +) -> anyhow::Result { use contexts::dsl; let mut new_override: Value = dsl::contexts .filter(dsl::id.eq(&ctx.id)) @@ -183,30 +233,20 @@ fn get_put_resp(ctx: Context) -> PutResp { priority: ctx.priority, } } -//TO-DO : Need to create a custom error type which implements error traits -#[derive(Debug)] -enum Error { - DIESEL(diesel::result::Error), - STRTYPE(String), -} fn put( req: web::Json, user: &User, conn: &mut PooledConnection>, already_under_txn: bool, -) -> Result { +) -> anyhow::Result { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, user).map_err(|e| { - log::error!("context struct creation failed with err: {e:?}"); - Error::STRTYPE(e.to_string()) - })?; + let new_ctx = create_ctx_from_put_req(req, conn, user)?; if already_under_txn { diesel::sql_query("SAVEPOINT put_ctx_savepoint") - .execute(conn) - .map_err(|e| Error::DIESEL(e))?; + .execute(conn)?; } let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); @@ -215,14 +255,13 @@ fn put( Err(DatabaseError(UniqueViolation, _)) => { if already_under_txn { diesel::sql_query("ROLLBACK TO put_ctx_savepoint") - .execute(conn) - .map_err(|e| Error::DIESEL(e))?; + .execute(conn)?; } - update_override_of_existing_ctx(conn, new_ctx).map_err(|e| Error::DIESEL(e)) + update_override_of_existing_ctx(conn, new_ctx) } Err(e) => { log::error!("update query failed with error: {e:?}"); - Err(Error::DIESEL(e)) + Err(anyhow!(e)) } } } @@ -251,25 +290,25 @@ fn r#move( user: &User, conn: &mut PooledConnection>, already_under_txn: bool, -) -> Result { +) -> anyhow::Result { use contexts::dsl; let req = req.into_inner(); let ctx_condition = Value::Object(req.context); let new_ctx_id = generate_context_id(&ctx_condition); - let priority = match val_dimensions_cal_priority(conn, &ctx_condition) { + let dimension_schema_map = get_all_dimension_schema_map(conn)?; + let priority = match validate_dimensions_and_calculate_priority(&ctx_condition, &dimension_schema_map) { Ok(0) => { - return Err(Error::STRTYPE("No dimension found in context".to_string())); + return Err(anyhow!(String::from("No dimension found in context"))); } Err(e) => { - return Err(Error::STRTYPE(e)); + return Err(anyhow!(e)); } Ok(p) => p, }; if already_under_txn { diesel::sql_query("SAVEPOINT update_ctx_savepoint") - .execute(conn) - .map_err(|e| Error::DIESEL(e))?; + .execute(conn)?; } let context = diesel::update(dsl::contexts) @@ -312,14 +351,13 @@ fn r#move( Err(DatabaseError(UniqueViolation, _)) => { if already_under_txn { diesel::sql_query("ROLLBACK TO update_ctx_savepoint") - .execute(conn) - .map_err(|e| Error::DIESEL(e))?; + .execute(conn)?; } - handle_unique_violation(conn, already_under_txn).map_err(|e| Error::DIESEL(e)) + handle_unique_violation(conn, already_under_txn) } Err(e) => { log::error!("update query failed with error: {e:?}"); - Err(Error::DIESEL(e)) + Err(anyhow!(e)) } } } @@ -517,4 +555,4 @@ async fn bulk_operations( Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index 5a39c87fc..044c6ed0b 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -12,6 +12,11 @@ pub struct MoveReq { pub context: Map, } +#[derive(Deserialize, Clone)] +pub struct DimensionCondition { + pub var: String, +} + #[derive(Serialize, Debug)] pub struct PutResp { pub context_id: String, @@ -37,4 +42,4 @@ pub enum ContextBulkResponse { PUT(PutResp), DELETE(String), MOVE(PutResp), -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 9a42b5524..ed9328a83 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -78,7 +78,7 @@ async fn create( Err(e) => { log::info!("Dimension upsert failed with error: {e}"); return HttpResponse::InternalServerError() - .body("Failed to create/udpate dimension\n"); + .body("Failed to create/update dimension\n"); } } } diff --git a/crates/context-aware-config/src/api/dimension/mod.rs b/crates/context-aware-config/src/api/dimension/mod.rs index ebe17b924..6088a1053 100644 --- a/crates/context-aware-config/src/api/dimension/mod.rs +++ b/crates/context-aware-config/src/api/dimension/mod.rs @@ -1,3 +1,5 @@ mod handlers; mod types; +mod utils; pub use handlers::endpoints; +pub use utils::get_all_dimension_schema_map; diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context-aware-config/src/api/dimension/utils.rs new file mode 100644 index 000000000..dd1610900 --- /dev/null +++ b/crates/context-aware-config/src/api/dimension/utils.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use crate::db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}; +use diesel::RunQueryDsl; +use jsonschema::{Draft, JSONSchema}; +use service_utils::{service::types::DBConnection}; + +pub fn get_all_dimension_schema_map( + conn: &mut DBConnection, +) -> anyhow::Result> { + let dimensions_vec = dimensions.load::(conn)?; + + let dimension_schema_map = dimensions_vec + .into_iter() + .filter_map(|item| { + let compiled_schema = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&item.schema) + .ok()?; + + Some((item.dimension, (compiled_schema, i32::from(item.priority)))) + }) + .collect(); + + Ok(dimension_schema_map) +} \ No newline at end of file diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index ae3c2baaf..2d006d518 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -97,7 +97,7 @@ pub fn get_meta_schema() -> JSONSchema { JSONSchema::options() .with_draft(Draft::Draft7) .compile(&my_schema) - .expect("Something weird happened, failed to compile the schema for the Dimension schema!") + .expect("Error encountered: Failed to compile 'context_dimension_schema_value'. Ensure it adheres to the correct format and data type.") } /* @@ -123,3 +123,56 @@ pub fn validate_jsonschema( }; res } + +// ************ Tests ************* + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_meta_schema() { + let x = get_meta_schema(); + let ok_string_validation = x + .validate(&json!({"type": "string", "pattern": ".*"})) + .map_err(|e| { + let verrors = e.collect::>(); + String::from(format!("Bad schema: {:?}", verrors.as_slice())) + }); + let error_string_validation = + match x.validate(&json!({"type": "string"})).map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating string dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }) { + Ok(()) => false, + Err(err_str) => err_str.contains("Bad schema"), + }; + + let error_object_validation = + match x.validate(&json!({"type": "object"})).map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating object dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }) { + Ok(()) => false, + Err(err_str) => err_str.contains("Bad schema"), + }; + let ok_enum_validation = x + .validate(&json!({"type": "string", "enum": ["ENUMVAL"]})) + .map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating enum dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }); + assert_eq!(ok_enum_validation, Ok(())); + assert_eq!(error_object_validation, true); + assert_eq!(ok_string_validation, Ok(())); + assert_eq!(error_string_validation, true); + } +} diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index ff5dcf26d..c5cbdf17c 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -28,6 +28,8 @@ pub struct AppState { pub snowflake_generator: Mutex, } +pub type DBConnection = PooledConnection>; + pub struct DbConnection(pub PooledConnection>); impl FromRequest for DbConnection { type Error = Error; diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index fab4a39cb..ddfc9dd5b 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -84,7 +84,7 @@ impl Client { running_experiments .iter() .filter(|(_, exp)| { - (jsonlogic::apply(&exp.context, context) == Ok(Value::Bool(true))) + jsonlogic::apply(&exp.context, context) == Ok(Value::Bool(true)) }) .map(|(_, exp)| exp.clone()) .collect::() From 6d09f90db01fc96256838958c89067d92bcb19c7 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 20 Oct 2023 13:02:24 +0000 Subject: [PATCH 157/352] chore(version): v0.10.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 6 +++--- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 4 ++-- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- crates/superposition_client/CHANGELOG.md | 6 ++++++ crates/superposition_client/Cargo.toml | 2 +- 8 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d113727e..5e893f790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.10.0 - 2023-10-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.9.0 +- service-utils bumped to service-utils-v0.6.0 +- superposition_client bumped to superposition_client-v0.2.0 +### Global changes + +- - - + ## v0.9.1 - 2023-10-13 ### Package updates - superposition_client bumped to superposition_client-v0.1.3 diff --git a/Cargo.lock b/Cargo.lock index cebb77762..0a0eb6f63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.8.0" +version = "0.9.0" dependencies = [ "actix", "actix-cors", @@ -3115,7 +3115,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix", "actix-web", @@ -3265,7 +3265,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.1.3" +version = "0.2.0" dependencies = [ "chrono", "dotenv", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 25ea656f5..8ca84b5a1 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.9.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + ## context-aware-config-v0.8.0 - 2023-10-10 #### Features - support to update experiment override_keys and variants - (9432bf7) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 1ed5c7cb7..7b0e509fb 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.8.0" +version = "0.9.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -56,4 +56,4 @@ futures-util = "0.3.28" external = { path = "../external"} actix-cors = "0.6.4" dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} -anyhow = { version = "1.0", default-features = false } \ No newline at end of file +anyhow = { version = "1.0", default-features = false } diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 9b1bc11cd..359c31ccd 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.6.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + ## service-utils-v0.5.0 - 2023-10-09 #### Features - server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index c2cff0948..5a33b67fa 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.5.0" +version = "0.6.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index 7dbbdc650..b0573158a 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.2.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + ## superposition_client-v0.1.3 - 2023-10-13 #### Bug Fixes - PICAF-24612 add all variants in manifest - (0f15ac9) - Pratik Mishra diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index b39acb878..b153a6fee 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.1.3" +version = "0.2.0" edition = "2021" [dependencies] From 2fa26ddd50f96a99d34c832571116fd0186a4a3c Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 15 Sep 2023 17:22:48 +0530 Subject: [PATCH 158/352] refactor!: moved tables and types out of cac_v1 schema --- crates/context-aware-config/diesel.toml | 3 +- .../up.sql | 70 +-- .../down.sql | 0 .../up.sql | 191 ++++++++ .../down.sql | 6 - .../up.sql | 36 -- .../down.sql | 1 - .../2023-05-24-134019_create_contexts/up.sql | 7 - .../down.sql | 2 - .../2023-05-25-111645_create_overrides/up.sql | 6 - .../down.sql | 2 - .../up.sql | 9 - .../down.sql | 1 - .../up.sql | 1 - .../down.sql | 1 - .../up.sql | 6 - .../down.sql | 2 - .../up.sql | 2 - .../down.sql | 7 - .../up.sql | 2 - .../down.sql | 9 - .../up.sql | 10 - .../down.sql | 3 - .../up.sql | 2 - .../down.sql | 7 - .../up.sql | 10 - .../2023-08-09-111620_add_audit_log/down.sql | 6 - .../v1/2023-08-09-111620_add_audit_log/up.sql | 130 ------ .../down.sql | 7 - .../up.sql | 5 - .../up.sql | 2 - .../src/api/audit_log/handlers.rs | 4 +- .../src/api/config/handlers.rs | 4 +- .../src/api/context/handlers.rs | 6 +- .../src/api/default_config/handlers.rs | 6 +- .../src/api/dimension/handlers.rs | 4 +- .../src/api/dimension/utils.rs | 2 +- crates/context-aware-config/src/db/models.rs | 4 +- crates/context-aware-config/src/db/schema.rs | 440 +++++++++--------- .../src/middlewares/audit_response_header.rs | 4 +- crates/experimentation-platform/diesel.toml | 3 +- .../down.sql | 3 - .../up.sql | 21 - .../2023-08-02-071224_add-index/down.sql | 5 - .../2023-08-02-071224_add-index/up.sql | 5 - .../down.sql | 4 - .../2023-08-04-080245_add-modified-by/up.sql | 5 - .../2023-08-09-081529_add_audit_log/down.sql | 3 - .../2023-08-09-081529_add_audit_log/up.sql | 4 - .../down.sql | 3 - .../up.sql | 3 - .../down.sql | 1 + .../up.sql | 184 ++++++++ .../src/api/experiments/handlers.rs | 8 +- .../src/api/experiments/helpers.rs | 2 +- .../experimentation-platform/src/db/models.rs | 6 +- .../experimentation-platform/src/db/schema.rs | 402 ++++++++-------- .../experimentation-platform/src/schema.patch | 49 +- makefile | 37 ++ 59 files changed, 881 insertions(+), 887 deletions(-) rename crates/context-aware-config/migrations/{v1/2023-10-09-101000_delete_old_default_configs_keys => 2023-10-16-133815_context-aware-config-init}/down.sql (100%) create mode 100644 crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql delete mode 100644 crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql delete mode 100644 crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql delete mode 100644 crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql delete mode 100644 crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql create mode 100644 crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql create mode 100644 crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql diff --git a/crates/context-aware-config/diesel.toml b/crates/context-aware-config/diesel.toml index 6192f8dbf..80d2be39f 100644 --- a/crates/context-aware-config/diesel.toml +++ b/crates/context-aware-config/diesel.toml @@ -1,6 +1,5 @@ [print_schema] file = "src/db/schema.rs" -schema = "cac_v1" [migrations_directory] -dir = "migrations/v1" +dir = "migrations" \ No newline at end of file diff --git a/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql index 73f54a278..d68895b1a 100644 --- a/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql +++ b/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql @@ -1,5 +1,21 @@ --- Setting up DB +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ BEGIN EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s @@ -11,58 +27,10 @@ CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ BEGIN IF ( NEW IS DISTINCT FROM OLD AND - NEW.last_modified IS NOT DISTINCT FROM OLD.last_modified + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at ) THEN - NEW.last_modified := current_timestamp; + NEW.updated_at := current_timestamp; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; - - - -CREATE TABLE IF NOT EXISTS dimensions ( - dimension VARCHAR NOT NULL, - priority integer NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(dimension) -); - -CREATE TABLE IF NOT EXISTS global_config ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE IF NOT EXISTS overrides ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(key) -); - -CREATE TABLE IF NOT EXISTS contexts ( - key VARCHAR NOT NULL, - value VARCHAR NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(key) -); - -CREATE TABLE ctxoverrides ( - context_id VARCHAR NOT NULL, - override_id VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(context_id) -); - -SELECT diesel_manage_updated_at('dimensions'); -SELECT diesel_manage_updated_at('global_config'); -SELECT diesel_manage_updated_at('overrides'); -SELECT diesel_manage_updated_at('contexts'); -SELECT diesel_manage_updated_at('ctxoverrides'); diff --git a/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql b/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/down.sql similarity index 100% rename from crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/down.sql rename to crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/down.sql diff --git a/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql b/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql new file mode 100644 index 000000000..08ce730c7 --- /dev/null +++ b/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql @@ -0,0 +1,191 @@ +-- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- +CREATE SCHEMA IF NOT EXISTS public; +-- +-- Name: dimension_type; Type: TYPE; Schema: public; Owner: - +-- +CREATE TYPE public.dimension_type AS ENUM ( + 'NULL', + 'BOOL', + 'NUMBER', + 'STRING', + 'ARRAY', + 'OBJECT' +); +-- +-- Name: event_logger(); Type: FUNCTION; Schema: public; Owner: - +-- +CREATE OR REPLACE FUNCTION public.event_logger() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + old_data json; + new_data json; +BEGIN + IF (TG_OP = 'UPDATE') THEN + old_data := row_to_json(OLD); + new_data := row_to_json(NEW); + INSERT INTO public.event_log + (table_name, user_name, action, original_data, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + new_data, + current_query() + ); + ELSIF (TG_OP = 'DELETE') THEN + old_data := row_to_json(OLD); + INSERT INTO public.event_log + (table_name, user_name, action, original_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + current_query() + ); + ELSIF (TG_OP = 'INSERT') THEN + new_data = row_to_json(NEW); + INSERT INTO public.event_log + (table_name, user_name, action, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + new_data, + current_query() + ); + END IF; + RETURN NULL; +END; +$$; +SET default_tablespace = ''; +SET default_table_access_method = heap; +-- +-- Name: contexts; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.contexts ( + id character varying PRIMARY KEY, + value json NOT NULL, + override_id character varying NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + priority integer DEFAULT 1 NOT NULL, + override json DEFAULT '{}'::json NOT NULL +); +-- +-- Name: default_configs; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.default_configs ( + key character varying PRIMARY KEY, + value json NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + schema json DEFAULT '{}'::json NOT NULL +); +-- +-- Name: dimensions; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.dimensions ( + dimension character varying PRIMARY KEY, + priority integer NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + schema json DEFAULT '{}'::json NOT NULL +); +-- +-- Name: event_log; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE IF NOT EXISTS public.event_log ( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + table_name text NOT NULL, + user_name text NOT NULL, + "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + action text NOT NULL, + original_data json, + new_data json, + query text NOT NULL, + PRIMARY KEY(id, timestamp) +) PARTITION BY RANGE ("timestamp"); + +-- +-- Name: event_log_action_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_action_index ON ONLY public.event_log USING btree (action) INCLUDE ("timestamp", table_name); +-- +-- Name: event_log_table_name_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_table_name_index ON ONLY public.event_log USING btree (table_name) INCLUDE (action, "timestamp"); +-- +-- Name: event_log_timestamp_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_timestamp_index ON ONLY public.event_log USING btree ("timestamp") INCLUDE (action, table_name); + +-- +-- Event log parititons +-- +CREATE TABLE IF NOT EXISTS public.event_log_y2023m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-08-01') TO ('2023-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-09-01') TO ('2023-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-10-01') TO ('2023-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-11-01') TO ('2023-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-12-01') TO ('2024-01-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-01-01') TO ('2024-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-02-01') TO ('2024-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-03-01') TO ('2024-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-04-01') TO ('2024-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-05-01') TO ('2024-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-06-01') TO ('2024-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-07-01') TO ('2024-08-01'); + +-- +-- Name: contexts contexts_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER contexts_audit AFTER INSERT OR DELETE OR UPDATE ON public.contexts FOR EACH ROW EXECUTE FUNCTION public.event_logger(); +-- +-- Name: default_configs default_configs_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER default_configs_audit AFTER INSERT OR DELETE OR UPDATE ON public.default_configs FOR EACH ROW EXECUTE FUNCTION public.event_logger(); +-- +-- Name: dimensions dimensions_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER dimensions_audit AFTER INSERT OR DELETE OR UPDATE ON public.dimensions FOR EACH ROW EXECUTE FUNCTION public.event_logger(); \ No newline at end of file diff --git a/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql b/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f526091..000000000 --- a/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql b/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1a..000000000 --- a/crates/context-aware-config/migrations/v1/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql b/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql deleted file mode 100644 index e72d46b4a..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE contexts; diff --git a/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql b/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql deleted file mode 100644 index 9aefc8902..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-24-134019_create_contexts/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE contexts ( - id VARCHAR PRIMARY KEY, - value JSON NOT NULL, - override_id VARCHAR NOT NULL, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR NOT NULL -); diff --git a/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql b/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql deleted file mode 100644 index e9fb2eaf0..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE overrides; diff --git a/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql b/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql deleted file mode 100644 index a941cb729..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-25-111645_create_overrides/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE overrides ( - id VARCHAR PRIMARY KEY, - value JSON NOT NULL, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR NOT NULL -); diff --git a/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql b/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql deleted file mode 100644 index ecf082d1b..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE dimensions; -DROP TYPE dimension_type; diff --git a/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql b/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql deleted file mode 100644 index 1f6118e4e..000000000 --- a/crates/context-aware-config/migrations/v1/2023-05-29-140304_create_dimensions/up.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TYPE dimension_type as enum ('NULL', 'BOOL', 'NUMBER', 'STRING', 'ARRAY', 'OBJECT'); - -CREATE TABLE dimensions ( - dimension VARCHAR PRIMARY KEY, - priority integer NOT NULL, - type dimension_type NOT NULL, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR NOT NULL -); diff --git a/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql b/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql deleted file mode 100644 index 7202c6857..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE contexts DROP COLUMN priority; diff --git a/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql b/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql deleted file mode 100644 index eeef787e6..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-12-070734_add_priority_to_contexts/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE contexts ADD priority integer NOT NULL DEFAULT 1; diff --git a/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql b/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql deleted file mode 100644 index edc7b697f..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE default_configs; diff --git a/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql b/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql deleted file mode 100644 index 128534967..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-14-054049_create-default_configs/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS default_configs ( - key varchar PRIMARY KEY, - value JSON NOT NULL, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by varchar NOT NULL -); diff --git a/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql b/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql deleted file mode 100644 index 3c1193ab7..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE default_configs - DROP COLUMN schema; diff --git a/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql b/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql deleted file mode 100644 index c46d86bf1..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-19-130600_add_schema_to_default_config/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE default_configs - ADD COLUMN schema JSON NOT NULL DEFAULT '{}'; diff --git a/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql b/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql deleted file mode 100644 index c071e0c3c..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/down.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE contexts DROP COLUMN override; -CREATE TABLE overrides ( - id VARCHAR PRIMARY KEY, - value JSON NOT NULL, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR NOT NULL -); diff --git a/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql b/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql deleted file mode 100644 index b4a531550..000000000 --- a/crates/context-aware-config/migrations/v1/2023-06-30-120629_move_override_to_contexts_table/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE contexts ADD override JSON NOT NULL DEFAULT '{}'; -DROP TABLE overrides; diff --git a/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql b/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql deleted file mode 100644 index a0881c043..000000000 --- a/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/down.sql +++ /dev/null @@ -1,9 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE cac_v1.contexts - SET SCHEMA public; - -ALTER TABLE cac_v1.default_configs - SET SCHEMA public; - -ALTER TABLE cac_v1.dimensions - SET SCHEMA public; diff --git a/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql b/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql deleted file mode 100644 index 3c8bba58e..000000000 --- a/crates/context-aware-config/migrations/v1/2023-07-19-134058_move-tables-to-cac_v1-schema/up.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS cac_v1; - -ALTER TABLE contexts - SET SCHEMA cac_v1; - -ALTER TABLE default_configs - SET SCHEMA cac_v1; - -ALTER TABLE dimensions - SET SCHEMA cac_v1; diff --git a/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql b/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql deleted file mode 100644 index f86859121..000000000 --- a/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TYPE dimension_type - SET SCHEMA public; diff --git a/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql b/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql deleted file mode 100644 index b3ed2962c..000000000 --- a/crates/context-aware-config/migrations/v1/2023-07-21-115432_move-dimension_type-to-cac_v1-schema/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TYPE dimension_type - SET SCHEMA cac_v1; diff --git a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql deleted file mode 100644 index 8268e5ac5..000000000 --- a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE cac_v1.dimensions DROP COLUMN schema; -ALTER TABLE cac_v1.dimensions ADD COLUMN type dimension_type NOT NULL DEFAULT 'STRING'; - -UPDATE cac_v1.dimensions SET type = 'NUMBER' WHERE dimension = 'toss'; -UPDATE cac_v1.dimensions SET type = 'NUMBER' WHERE dimension = 'tier'; -UPDATE cac_v1.dimensions SET type = 'BOOL' WHERE dimension = 'internalUser'; diff --git a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql b/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql deleted file mode 100644 index 8166f50a6..000000000 --- a/crates/context-aware-config/migrations/v1/2023-08-03-100512_alter_type_schema_column_in_dimension/up.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Your SQL goes here -ALTER TABLE cac_v1.dimensions ADD COLUMN schema JSON NOT NULL DEFAULT '{}'; -ALTER TABLE cac_v1.dimensions DROP COLUMN type; - -UPDATE cac_v1.dimensions SET schema = '{"enum":["android","ios","web"],"type":"string"}' WHERE dimension = 'os'; -UPDATE cac_v1.dimensions SET schema = '{"type":"number"}' WHERE dimension = 'toss'; -UPDATE cac_v1.dimensions SET schema = '{"pattern":"^[a-z0-9].*$","type":"string"}' WHERE dimension = 'clientId'; -UPDATE cac_v1.dimensions SET schema = '{"enum":["beta","release","cug"],"type":"string"}' WHERE dimension = 'scope'; -UPDATE cac_v1.dimensions SET schema = '{"type":"boolean"}' WHERE dimension = 'internalUser'; -UPDATE cac_v1.dimensions SET schema = '{"type":"number"}' WHERE dimension = 'tier'; diff --git a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql deleted file mode 100644 index 7d1b18662..000000000 --- a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TRIGGER contexts_audit ON cac_v1.contexts; -DROP TRIGGER dimensions_audit ON cac_v1.dimensions; -DROP TRIGGER default_configs_audit ON cac_v1.default_configs; - -DROP TABLE cac_v1.event_log; diff --git a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql b/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql deleted file mode 100644 index 943461a04..000000000 --- a/crates/context-aware-config/migrations/v1/2023-08-09-111620_add_audit_log/up.sql +++ /dev/null @@ -1,130 +0,0 @@ --- Your SQL goes here -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE TABLE cac_v1.event_log ( - id UUID DEFAULT uuid_generate_v4(), - table_name TEXT NOT NULL, - user_name TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - action TEXT NOT NULL, - original_data json, - new_data json, - query TEXT NOT NULL, - PRIMARY KEY(id, timestamp) -) PARTITION BY RANGE (timestamp); - -CREATE INDEX event_log_table_name_index ON cac_v1.event_log (table_name) INCLUDE (action, timestamp); -CREATE INDEX event_log_timestamp_index ON cac_v1.event_log (timestamp) INCLUDE (action, table_name); -CREATE INDEX event_log_action_index on cac_v1.event_log (action) INCLUDE (timestamp, table_name); - - -CREATE OR REPLACE FUNCTION cac_v1.event_logger() RETURNS TRIGGER AS $$ -DECLARE - old_data json; - new_data json; -BEGIN - IF (TG_OP = 'UPDATE') THEN - old_data := row_to_json(OLD); - new_data := row_to_json(NEW); - - INSERT INTO cac_v1.event_log - (table_name, user_name, action, original_data, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - new_data, - current_query() - ); - - ELSIF (TG_OP = 'DELETE') THEN - old_data := row_to_json(OLD); - - INSERT INTO cac_v1.event_log - (table_name, user_name, action, original_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - current_query() - ); - - ELSIF (TG_OP = 'INSERT') THEN - new_data = row_to_json(NEW); - - INSERT INTO cac_v1.event_log - (table_name, user_name, action, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - new_data, - current_query() - ); - END IF; - - RETURN NULL; -END; -$$ LANGUAGE 'plpgsql'; - -CREATE TRIGGER contexts_audit -AFTER INSERT OR UPDATE OR DELETE ON cac_v1.contexts -FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); - -CREATE TRIGGER dimensions_audit -AFTER INSERT OR UPDATE OR DELETE ON cac_v1.dimensions -FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); - -CREATE TRIGGER default_configs_audit -AFTER INSERT OR UPDATE OR DELETE ON cac_v1.default_configs -FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); - --- Partitions for event_log table -CREATE TABLE cac_v1.event_log_y2023m08 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2023-08-01') TO ('2023-09-01'); - -CREATE TABLE cac_v1.event_log_y2023m09 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2023-09-01') TO ('2023-10-01'); - -CREATE TABLE cac_v1.event_log_y2023m10 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2023-10-01') TO ('2023-11-01'); - -CREATE TABLE cac_v1.event_log_y2023m11 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2023-11-01') TO ('2023-12-01'); - -CREATE TABLE cac_v1.event_log_y2023m12 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2023-12-01') TO ('2024-01-01'); - -CREATE TABLE cac_v1.event_log_y2024m01 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-01-01') TO ('2024-02-01'); - -CREATE TABLE cac_v1.event_log_y2024m02 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-02-01') TO ('2024-03-01'); - -CREATE TABLE cac_v1.event_log_y2024m03 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-03-01') TO ('2024-04-01'); - -CREATE TABLE cac_v1.event_log_y2024m04 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-04-01') TO ('2024-05-01'); - -CREATE TABLE cac_v1.event_log_y2024m05 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-05-01') TO ('2024-06-01'); - -CREATE TABLE cac_v1.event_log_y2024m06 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-06-01') TO ('2024-07-01'); - -CREATE TABLE cac_v1.event_log_y2024m07 PARTITION OF cac_v1.event_log FOR -VALUES -FROM ('2024-07-01') TO ('2024-08-01'); diff --git a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql deleted file mode 100644 index 1c2134c45..000000000 --- a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/down.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE cac_v1.dimensions SET priority=1 where dimension='os'; -UPDATE cac_v1.dimensions SET priority=32 where dimension='internalUser'; -UPDATE cac_v1.dimensions SET priority=64 where dimension='variantIds'; -INSERT INTO cac_v1.dimensions (dimension, priority, created_by, schema) - VALUES ('toss', '2', 'migration_down@juspay.in', '{"type":"number"}'); -INSERT INTO cac_v1.dimensions (dimension, priority, created_by, schema) - VALUES ('scope', '16', 'migration_down@juspay.in', '{"enum":["beta","release","cug"],"type":"string"}'); \ No newline at end of file diff --git a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql b/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql deleted file mode 100644 index c85b5b1ee..000000000 --- a/crates/context-aware-config/migrations/v1/2023-10-05-095010_dimensions-priority-update/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM cac_v1.dimensions where dimension='toss'; -DELETE FROM cac_v1.dimensions where dimension='scope'; -UPDATE cac_v1.dimensions SET priority=1 where dimension='variantIds'; -UPDATE cac_v1.dimensions SET priority=2 where dimension='os'; -UPDATE cac_v1.dimensions SET priority=16 where dimension='internalUser'; \ No newline at end of file diff --git a/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql b/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql deleted file mode 100644 index dfb5e9dcb..000000000 --- a/crates/context-aware-config/migrations/v1/2023-10-09-101000_delete_old_default_configs_keys/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -delete from cac_v1.default_configs where key in ('dependency_hyperpay','ec_base_html_android', 'ec_base_html_ios', 'godel_acs_android', 'godel_bundle_android', 'godel_config_android', 'godel_placeholder_acs_android', 'godel_placeholder_bundle_android', 'godel_placeholder_config_android', 'hyperos_placeholder_tracker_android', 'hyperos_placeholder_tracker_ios', 'hyperos_placeholder_tracker_web', 'hyperpay_base_html_android', 'hyperpay_base_html_ios', 'patch_entries', 'upi_base_html_ios', 'upi_intent_base_html_android', 'upi_intent_bundle_web', 'upi_intent_config_web'); \ No newline at end of file diff --git a/crates/context-aware-config/src/api/audit_log/handlers.rs b/crates/context-aware-config/src/api/audit_log/handlers.rs index 6396e51bc..610451b8a 100644 --- a/crates/context-aware-config/src/api/audit_log/handlers.rs +++ b/crates/context-aware-config/src/api/audit_log/handlers.rs @@ -6,7 +6,7 @@ use service_utils::{service::types::DbConnection, types as app}; use crate::{api::audit_log::types::AuditQueryFilters, db::models::EventLog}; -use crate::db::schema::cac_v1::event_log::dsl as event_log; +use crate::db::schema::event_log::dsl as event_log; pub fn endpoints() -> Scope { Scope::new("").service(get_audit_logs) @@ -60,4 +60,4 @@ async fn get_audit_logs( "total_pages": total_pages, "data": logs }))) -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 187a02a46..87146f9eb 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use super::types::Config; -use crate::db::schema::cac_v1::{ +use crate::db::schema::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; use actix_web::{ @@ -142,4 +142,4 @@ async fn get_resolved_config( ) .map_err_to_internal_server("cac eval failed", Null)?, )) -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 961fb1089..bc8cd8b34 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -10,7 +10,7 @@ use crate::{ }, db::{ models::Context, - schema::cac_v1::{ + schema::{ contexts::{self, id}, default_configs::dsl, }, @@ -387,7 +387,7 @@ async fn get_context( path: web::Path, state: Data, ) -> Result { - use crate::db::schema::cac_v1::contexts::dsl::*; + use crate::db::schema::contexts::dsl::*; let ctx_id = path.into_inner(); let mut conn = match state.db_pool.get() { @@ -420,7 +420,7 @@ async fn list_contexts( qparams: web::Query, state: Data, ) -> Result { - use crate::db::schema::cac_v1::contexts::dsl::*; + use crate::db::schema::contexts::dsl::*; let mut conn = state .db_pool diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 195f9f48e..eba094670 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,7 +1,7 @@ use super::types::CreateReq; use crate::{ - db::{models::DefaultConfig, schema::cac_v1::default_configs::dsl::default_configs}, - helpers::validate_jsonschema, + db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}, + helpers::validate_jsonschema }; use actix_web::{ put, @@ -84,4 +84,4 @@ async fn create( .body("Failed to create DefaultConfig"); } } -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index ed9328a83..6d45a2107 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -1,6 +1,6 @@ use crate::{ api::dimension::types::CreateReq, - db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}, + db::{models::Dimension, schema::dimensions::dsl::*}, helpers::validate_jsonschema, }; use actix_web::{ @@ -81,4 +81,4 @@ async fn create( .body("Failed to create/update dimension\n"); } } -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context-aware-config/src/api/dimension/utils.rs index dd1610900..790889cd9 100644 --- a/crates/context-aware-config/src/api/dimension/utils.rs +++ b/crates/context-aware-config/src/api/dimension/utils.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::db::{models::Dimension, schema::cac_v1::dimensions::dsl::*}; +use crate::db::{models::Dimension, schema::dimensions::dsl::*}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use service_utils::{service::types::DBConnection}; diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index dc4eea716..53a0626c0 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -1,4 +1,4 @@ -use crate::db::schema::cac_v1::{contexts, default_configs, dimensions, event_log}; +use crate::db::schema::{contexts, default_configs, dimensions, event_log}; use chrono::{offset::Utc, DateTime, NaiveDateTime}; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; use serde::Serialize; @@ -53,4 +53,4 @@ pub struct EventLog { pub original_data: Option, pub new_data: Option, pub query: String, -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 565173117..6a60f2bf3 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -1,224 +1,222 @@ // @generated automatically by Diesel CLI. -pub mod cac_v1 { - diesel::table! { - cac_v1.contexts (id) { - id -> Varchar, - value -> Json, - override_id -> Varchar, - created_at -> Timestamptz, - created_by -> Varchar, - priority -> Int4, - #[sql_name = "override"] - override_ -> Json, - } - } - - diesel::table! { - cac_v1.default_configs (key) { - key -> Varchar, - value -> Json, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } - } - - diesel::table! { - cac_v1.dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } - } - - diesel::table! { - cac_v1.event_log (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2023m08 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2023m09 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2023m10 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2023m11 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2023m12 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m01 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m02 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m03 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m04 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m05 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m06 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::table! { - cac_v1.event_log_y2024m07 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } - } - - diesel::allow_tables_to_appear_in_same_query!( - contexts, - default_configs, - dimensions, - event_log, - event_log_y2023m08, - event_log_y2023m09, - event_log_y2023m10, - event_log_y2023m11, - event_log_y2023m12, - event_log_y2024m01, - event_log_y2024m02, - event_log_y2024m03, - event_log_y2024m04, - event_log_y2024m05, - event_log_y2024m06, - event_log_y2024m07, - ); +diesel::table! { + contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + priority -> Int4, + #[sql_name = "override"] + override_ -> Json, + } +} + +diesel::table! { + default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } +} + +diesel::table! { + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } +} + +diesel::table! { + event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } } + +diesel::table! { + event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, +); diff --git a/crates/context-aware-config/src/middlewares/audit_response_header.rs b/crates/context-aware-config/src/middlewares/audit_response_header.rs index 0093d6096..a6efb9309 100644 --- a/crates/context-aware-config/src/middlewares/audit_response_header.rs +++ b/crates/context-aware-config/src/middlewares/audit_response_header.rs @@ -10,7 +10,7 @@ use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use futures_util::future::LocalBoxFuture; use service_utils::service::types::AppState; -use crate::db::schema::cac_v1::event_log::dsl as event_log; +use crate::db::schema::event_log::dsl as event_log; use uuid::Uuid; #[derive(Clone, Copy, Debug, strum_macros::Display)] @@ -109,4 +109,4 @@ where Ok(res) }) } -} +} \ No newline at end of file diff --git a/crates/experimentation-platform/diesel.toml b/crates/experimentation-platform/diesel.toml index 0c229d58d..4f544dd62 100644 --- a/crates/experimentation-platform/diesel.toml +++ b/crates/experimentation-platform/diesel.toml @@ -1,7 +1,6 @@ [print_schema] file = "src/db/schema.rs" -schema = "cac_v1" patch_file = "src/schema.patch" [migrations_directory] -dir = "migrations" +dir = "migrations" \ No newline at end of file diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql deleted file mode 100644 index e0f8b9396..000000000 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE cac_v1.experiments; -DROP TYPE cac_v1.experiment_status_type; diff --git a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql b/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql deleted file mode 100644 index 7f759cf70..000000000 --- a/crates/experimentation-platform/migrations/2023-07-14-093533_create_experiments/up.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Your SQL goes here -CREATE TYPE cac_v1.experiment_status_type as enum ('CREATED', 'CONCLUDED', 'INPROGRESS'); -CREATE DOMAIN cac_v1.not_null_text as TEXT NOT NULL; - -CREATE TABLE cac_v1.experiments ( - id BIGINT PRIMARY KEY, - created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL, - last_modified timestamp WITH time zone, - - name TEXT NOT NULL, - override_keys cac_v1.not_null_text [] NOT NULL, - status cac_v1.experiment_status_type NOT NULL, - traffic_percentage INTEGER NOT NULL CHECK (traffic_percentage >= 0), - - context JSON NOT NULL, - variants JSON NOT NULL -); - -CREATE INDEX experiment_created_date_index ON cac_v1.experiments (created_at) INCLUDE (id); -CREATE INDEX experiment_last_modified_index ON cac_v1.experiments (last_modified) INCLUDE (id, created_at); diff --git a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql deleted file mode 100644 index 395b03b1c..000000000 --- a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE cac_v1.experiments -ALTER COLUMN last_modified DROP NOT NULL, -ALTER COLUMN last_modified DROP DEFAULT; -DROP INDEX experiment_status_index; diff --git a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql b/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql deleted file mode 100644 index b2d158090..000000000 --- a/crates/experimentation-platform/migrations/2023-08-02-071224_add-index/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here -ALTER TABLE cac_v1.experiments -ALTER COLUMN last_modified SET NOT NULL, -ALTER COLUMN last_modified SET DEFAULT NOW(); -CREATE INDEX experiment_status_index ON cac_v1.experiments (status) INCLUDE (created_at, last_modified); diff --git a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql deleted file mode 100644 index 30947823d..000000000 --- a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- This file should undo anything in `up.sql` - -ALTER TABLE cac_v1.experiments -DROP COLUMN last_modified_by; diff --git a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql b/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql deleted file mode 100644 index 430339eb0..000000000 --- a/crates/experimentation-platform/migrations/2023-08-04-080245_add-modified-by/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here - --- Your SQL goes here -ALTER TABLE cac_v1.experiments -ADD COLUMN last_modified_by TEXT NOT NULL DEFAULT 'Null'; diff --git a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql deleted file mode 100644 index 61c1ea1f9..000000000 --- a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TRIGGER experiments_audit ON cac_v1.experiments; - diff --git a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql b/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql deleted file mode 100644 index c417a0786..000000000 --- a/crates/experimentation-platform/migrations/2023-08-09-081529_add_audit_log/up.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Your SQL goes here -CREATE TRIGGER experiments_audit -AFTER INSERT OR UPDATE OR DELETE ON cac_v1.experiments -FOR EACH ROW EXECUTE PROCEDURE cac_v1.event_logger(); diff --git a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql deleted file mode 100644 index 5dcf1f5c6..000000000 --- a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE cac_v1.experiments -DROP COLUMN chosen_variant; diff --git a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql b/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql deleted file mode 100644 index 70cddb557..000000000 --- a/crates/experimentation-platform/migrations/2023-08-25-094017_PICAF-24160-save-winner-variant/up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Your SQL goes here -ALTER TABLE cac_v1.experiments -ADD COLUMN chosen_variant TEXT; diff --git a/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql b/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql new file mode 100644 index 000000000..291a97c5c --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql b/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql new file mode 100644 index 000000000..f44d33025 --- /dev/null +++ b/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql @@ -0,0 +1,184 @@ +-- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- +CREATE SCHEMA IF NOT EXISTS public; +-- +-- Name: experiment_status_type; Type: TYPE; Schema: public; Owner: - +-- +CREATE TYPE public.experiment_status_type AS ENUM ( + 'CREATED', + 'CONCLUDED', + 'INPROGRESS' +); +-- +-- Name: not_null_text; Type: DOMAIN; Schema: public; Owner: - +-- +CREATE DOMAIN public.not_null_text AS text NOT NULL; +-- +-- Name: event_logger(); Type: FUNCTION; Schema: public; Owner: - +-- +CREATE OR REPLACE FUNCTION public.event_logger() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + old_data json; + new_data json; +BEGIN + IF (TG_OP = 'UPDATE') THEN + old_data := row_to_json(OLD); + new_data := row_to_json(NEW); + INSERT INTO public.event_log + (table_name, user_name, action, original_data, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + new_data, + current_query() + ); + ELSIF (TG_OP = 'DELETE') THEN + old_data := row_to_json(OLD); + INSERT INTO public.event_log + (table_name, user_name, action, original_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + current_query() + ); + ELSIF (TG_OP = 'INSERT') THEN + new_data = row_to_json(NEW); + INSERT INTO public.event_log + (table_name, user_name, action, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + new_data, + current_query() + ); + END IF; + RETURN NULL; +END; +$$; +SET default_tablespace = ''; +SET default_table_access_method = heap; +-- +-- Name: experiments; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.experiments ( + id bigint PRIMARY KEY, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by text NOT NULL, + last_modified timestamp with time zone DEFAULT now() NOT NULL, + name text NOT NULL, + override_keys public.not_null_text[] NOT NULL, + status public.experiment_status_type NOT NULL, + traffic_percentage integer NOT NULL, + context json NOT NULL, + variants json NOT NULL, + last_modified_by text DEFAULT 'Null'::text NOT NULL, + chosen_variant text, + CONSTRAINT experiments_traffic_percentage_check CHECK ((traffic_percentage >= 0)) +); +-- +-- Name: experiment_created_date_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX experiment_created_date_index ON public.experiments USING btree (created_at) INCLUDE (id); +-- +-- Name: experiment_last_modified_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX experiment_last_modified_index ON public.experiments USING btree (last_modified) INCLUDE (id, created_at); +-- +-- Name: experiment_status_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX experiment_status_index ON public.experiments USING btree (status) INCLUDE (created_at, last_modified); + +-- +-- Name: event_log; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE IF NOT EXISTS public.event_log ( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + table_name text NOT NULL, + user_name text NOT NULL, + "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + action text NOT NULL, + original_data json, + new_data json, + query text NOT NULL, + PRIMARY KEY(id, timestamp) +) +PARTITION BY RANGE ("timestamp"); + +-- +-- Name: event_log_action_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_action_index ON ONLY public.event_log USING btree (action) INCLUDE ("timestamp", table_name); +-- +-- Name: event_log_table_name_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_table_name_index ON ONLY public.event_log USING btree (table_name) INCLUDE (action, "timestamp"); +-- +-- Name: event_log_timestamp_index; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX IF NOT EXISTS event_log_timestamp_index ON ONLY public.event_log USING btree ("timestamp") INCLUDE (action, table_name); + +-- +-- event_log table partitions +-- +CREATE TABLE IF NOT EXISTS public.event_log_y2023m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-08-01') TO ('2023-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-09-01') TO ('2023-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-10-01') TO ('2023-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-11-01') TO ('2023-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2023m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2023-12-01') TO ('2024-01-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-01-01') TO ('2024-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-02-01') TO ('2024-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-03-01') TO ('2024-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-04-01') TO ('2024-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-05-01') TO ('2024-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-06-01') TO ('2024-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-07-01') TO ('2024-08-01'); + +-- +-- Name: experiments experiments_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER experiments_audit AFTER INSERT OR DELETE OR UPDATE ON public.experiments FOR EACH ROW EXECUTE FUNCTION public.event_logger(); \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index a9bf4ae7c..2532a5cdc 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -36,7 +36,7 @@ use super::{ use crate::{ db::models::{Experiment, ExperimentStatusType}, - db::schema::cac_v1::{event_log::dsl as event_log, experiments::dsl as experiments}, + db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, }; use serde_json::{json, Map, Value}; @@ -60,7 +60,7 @@ async fn create( db_conn: DbConnection, tenant: Tenant, ) -> app::Result> { - use crate::db::schema::cac_v1::experiments::dsl::experiments; + use crate::db::schema::experiments::dsl::experiments; let mut variants = req.variants.to_vec(); let DbConnection(mut conn) = db_conn; @@ -237,7 +237,7 @@ pub async fn conclude( user: User, tenant: Tenant, ) -> app::Result { - use crate::db::schema::cac_v1::experiments::dsl; + use crate::db::schema::experiments::dsl; let winner_variant_id: String = req.chosen_variant.to_owned(); @@ -408,7 +408,7 @@ pub fn get_experiment( experiment_id: i64, conn: &mut PooledConnection>, ) -> app::Result { - use crate::db::schema::cac_v1::experiments::dsl::*; + use crate::db::schema::experiments::dsl::*; let result: Experiment = experiments .find(experiment_id) .get_result::(conn)?; diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index d3c7afd99..3b1e3714b 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -261,7 +261,7 @@ pub fn validate_experiment( flags: &ExperimentationFlags, conn: &mut PgConnection, ) -> app::Result<(bool, String)> { - use crate::db::schema::cac_v1::experiments::dsl as experiments_dsl; + use crate::db::schema::experiments::dsl as experiments_dsl; let active_experiments: Vec = experiments_dsl::experiments .filter( diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index 6194aae5c..bef3cd9bc 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -1,4 +1,4 @@ -use crate::db::schema::cac_v1::*; +use crate::db::schema::*; use chrono::{DateTime, Utc}; use diesel::{Insertable, Queryable, QueryableByName, Selectable}; @@ -9,7 +9,7 @@ use serde_json::Value; Debug, Clone, Copy, PartialEq, Deserialize, Serialize, diesel_derive_enum::DbEnum, )] #[DbValueStyle = "UPPERCASE"] -#[ExistingTypePath = "crate::db::schema::cac_v1::sql_types::ExperimentStatusType"] +#[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] pub enum ExperimentStatusType { CREATED, CONCLUDED, @@ -36,4 +36,4 @@ pub struct Experiment { pub chosen_variant: Option, } -pub type Experiments = Vec; +pub type Experiments = Vec; \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 187ace544..46a426c1e 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -1,251 +1,213 @@ // @generated automatically by Diesel CLI. -pub mod cac_v1 { - pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] - pub struct ExperimentStatusType; - } - - diesel::table! { - cac_v1.contexts (id) { - id -> Varchar, - value -> Json, - override_id -> Varchar, - created_at -> Timestamptz, - created_by -> Varchar, - priority -> Int4, - #[sql_name = "override"] - override_ -> Json, - } - } - - diesel::table! { - cac_v1.default_configs (key) { - key -> Varchar, - value -> Json, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } - } - - diesel::table! { - cac_v1.dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } - } +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "experiment_status_type"))] + pub struct ExperimentStatusType; +} - diesel::table! { - cac_v1.event_log (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2023m08 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2023m09 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2023m10 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2023m11 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2023m12 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m01 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m02 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m03 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m04 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m05 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m06 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - cac_v1.event_log_y2024m07 (id, timestamp) { - id -> Uuid, - table_name -> Text, - user_name -> Text, - timestamp -> Timestamp, - action -> Text, - original_data -> Nullable, - new_data -> Nullable, - query -> Text, - } +diesel::table! { + event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, } +} - diesel::table! { - use diesel::sql_types::*; - use super::sql_types::ExperimentStatusType; - - cac_v1.experiments (id) { - id -> Int8, - created_at -> Timestamptz, - created_by -> Text, - last_modified -> Timestamptz, - name -> Text, - override_keys -> Array, - status -> ExperimentStatusType, - traffic_percentage -> Int4, - context -> Json, - variants -> Json, - last_modified_by -> Text, - chosen_variant -> Nullable, - } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ExperimentStatusType; + + experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + last_modified -> Timestamptz, + name -> Text, + override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + last_modified_by -> Text, + chosen_variant -> Nullable, } - - diesel::allow_tables_to_appear_in_same_query!( - contexts, - default_configs, - dimensions, - event_log, - event_log_y2023m08, - event_log_y2023m09, - event_log_y2023m10, - event_log_y2023m11, - event_log_y2023m12, - event_log_y2024m01, - event_log_y2024m02, - event_log_y2024m03, - event_log_y2024m04, - event_log_y2024m05, - event_log_y2024m06, - event_log_y2024m07, - experiments, - ); } + +diesel::allow_tables_to_appear_in_same_query!( + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, + experiments, +); \ No newline at end of file diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation-platform/src/schema.patch index 0fc396d2f..b9e8334f3 100644 --- a/crates/experimentation-platform/src/schema.patch +++ b/crates/experimentation-platform/src/schema.patch @@ -1,32 +1,25 @@ diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs -index 65aa8b6..722a39a 100644 +index 692c718..46a426c 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs -@@ -9,10 +9,6 @@ pub mod cac_v1 { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "experiment_status_type", schema = "cac_v1"))] - pub struct ExperimentStatusType; +@@ -6,6 +6,2 @@ pub mod sql_types { + pub struct ExperimentStatusType; - -- #[derive(diesel::sql_types::SqlType)] -- #[diesel(postgres_type(name = "not_null_text", schema = "cac_v1"))] -- pub struct NotNullText; - } - - diesel::table! { -@@ -54,7 +50,6 @@ pub mod cac_v1 { - - diesel::table! { - use diesel::sql_types::*; -- use super::sql_types::NotNullText; - use super::sql_types::ExperimentStatusType; - - cac_v1.experiments (id) { -@@ -63,7 +58,7 @@ pub mod cac_v1 { - created_by -> Text, - last_modified -> Timestamptz, - name -> Text, -- override_keys -> Array>, -+ override_keys -> Array, - status -> ExperimentStatusType, - traffic_percentage -> Int4, - context -> Json, +- #[derive(diesel::sql_types::SqlType)] +- #[diesel(postgres_type(name = "not_null_text"))] +- pub struct NotNullText; + } +@@ -183,3 +179,2 @@ diesel::table! { + use diesel::sql_types::*; +- use super::sql_types::NotNullText; + use super::sql_types::ExperimentStatusType; +@@ -192,3 +187,3 @@ diesel::table! { + name -> Text, +- override_keys -> Array>, ++ override_keys -> Array, + status -> ExperimentStatusType, +@@ -217,2 +212,2 @@ diesel::allow_tables_to_appear_in_same_query!( + experiments, +-); ++); +\ No newline at end of file diff --git a/makefile b/makefile index a8e3cfd59..730764b58 100644 --- a/makefile +++ b/makefile @@ -16,8 +16,38 @@ SHELL := /usr/bin/env bash cac db-init: + diesel migration run --locked-schema --config-file=crates/context-aware-config/diesel.toml + -diesel migration run --locked-schema --config-file=crates/experimentation-platform/diesel.toml + +cac-migration: + -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) + -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") + docker-compose up -d postgres + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection; \ + do echo "waiting for postgres bootup"; \ + sleep 0.5; \ + done diesel migration run --config-file=crates/context-aware-config/diesel.toml + docker-compose down + +exp-migration: + -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) + -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") + docker-compose up -d postgres + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection; \ + do echo "waiting for postgres bootup"; \ + sleep 0.5; \ + done diesel migration run --config-file=crates/experimentation-platform/diesel.toml + docker-compose down + +migration: + make cac-migration + make exp-migration validate-aws-connection: aws --endpoint-url=http://$(DOCKER_DNS):4566 --region=ap-south-1 sts get-caller-identity @@ -26,6 +56,10 @@ validate-psql-connection: pg_isready -h $(DOCKER_DNS) -p 5432 setup: + # NOTE: `make migration` is being used to run the migrations for cac and experimentation in isolation, + # otherwise the tables and types of cac and experimentation spill into each others schema.rs + # NOTE: The container spinned up are stopped and removed after the work is done. + make migration docker-compose up -d postgres localstack cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -33,6 +67,9 @@ setup: do echo "waiting for postgres, localstack bootup"; \ sleep 0.5; \ done + # NOTE: `make db-init` finally starts a postgres container and runs all the migrations with locked-schema option + # to prevent update of schema.rs for both cac and experimentation. + # NOTE: The container spinned-up here is the actual container being used in development make db-init kill: From df2edb698b93d621eca5e57fab93b9064211bacb Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 18 Sep 2023 19:11:01 +0530 Subject: [PATCH 159/352] feat: added middleware and FromRequest for tenant and app scope info --- .env.example | 5 +- Cargo.lock | 14 +- crates/context-aware-config/src/main.rs | 65 ++++-- crates/experimentation-platform/Cargo.toml | 4 +- .../src/api/experiments/handlers.rs | 55 ++++- .../src/api/experiments/types.rs | 21 +- .../experimentation-platform/src/db/models.rs | 19 +- crates/service-utils/Cargo.toml | 2 + crates/service-utils/src/errors/types.rs | 3 +- crates/service-utils/src/lib.rs | 5 +- .../src/middlewares/app_scope.rs | 66 ++++++ crates/service-utils/src/middlewares/mod.rs | 1 + crates/service-utils/src/service/types.rs | 76 +++++- crates/service-utils/src/types.rs | 1 - makefile | 12 +- scripts/cac-init.sql | 221 ++++++++++++++++++ scripts/create-tenant.sh | 29 +++ scripts/experimentation-init.sql | 187 +++++++++++++++ 18 files changed, 745 insertions(+), 41 deletions(-) create mode 100644 crates/service-utils/src/middlewares/app_scope.rs create mode 100644 crates/service-utils/src/middlewares/mod.rs create mode 100644 scripts/cac-init.sql create mode 100755 scripts/create-tenant.sh create mode 100644 scripts/experimentation-init.sql diff --git a/.env.example b/.env.example index cc5bf10cc..ab355761a 100644 --- a/.env.example +++ b/.env.example @@ -22,4 +22,7 @@ TENANT_VALIDATION_ENABLED=false AUTH_EXCLUSION_LIST="GET /context/list,GET /context/{ctx_id},GET /config/resolve,GET /config,GET /audit,GET /experiments,GET /experiments/{id}" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" ACTIX_KEEP_ALIVE=120 -MAX_DB_CONNECTION_POOL_SIZE=3 \ No newline at end of file +MAX_DB_CONNECTION_POOL_SIZE=3 +PROD=false +ENABLE_TENANT_AND_SCOPE=false +TENANTS=tenant1,tenant2,tenant3 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0a0eb6f63..799396712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,7 +1201,7 @@ dependencies = [ "service-utils", "strum", "strum_macros", - "uuid 0.8.2", + "uuid 1.3.4", ] [[package]] @@ -3121,9 +3121,11 @@ dependencies = [ "actix-web", "base64 0.21.2", "bytes 1.4.0", + "dashboard-auth", "derive_more", "diesel", "dotenv", + "futures-util", "jsonschema", "log", "rs-snowflake", @@ -3901,16 +3903,6 @@ dependencies = [ "rand 0.6.5", ] -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom", - "serde", -] - [[package]] name = "uuid" version = "1.3.4" diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index bb131504e..1fc2d7793 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -12,16 +12,19 @@ use dotenv; use experimentation_platform::api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; use logger::{init_log_subscriber, CustomRootSpanBuilder}; +use std::{env, io::Result, collections::HashSet}; +use tracing::{span, Level}; + +use snowflake::SnowflakeIdGenerator; +use std::{sync::Mutex, time::Duration}; +use tracing_actix_web::TracingLogger; + use service_utils::{ db::utils::get_pool, helpers::{get_from_env_unsafe, get_pod_info}, - service::types::{AppState, ExperimentationFlags}, + middlewares::app_scope::AppExecutionScope, + service::types::{AppScope, AppState, ExperimentationFlags}, }; -use snowflake::SnowflakeIdGenerator; -use std::{env, io::Result}; -use std::{sync::Mutex, time::Duration}; -use tracing::{span, Level}; -use tracing_actix_web::TracingLogger; #[actix_web::main] async fn main() -> Result<()> { @@ -42,6 +45,15 @@ async fn main() -> Result<()> { let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let prod: bool = get_from_env_unsafe("PROD").expect("PROD is not set"); + let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") + .expect("ENABLE_TENANT_AND_SCOPE is not set"); + let tenants: HashSet = get_from_env_unsafe::("TENANTS") + .expect("TENANTS is not set") + .split(",") + .map(|tenant| tenant.to_string()) + .collect::>(); + let string_to_int = |s: &String| -> i32 { s.chars() .map(|i| (i as i32) & rand::random::()) @@ -88,6 +100,9 @@ async fn main() -> Result<()> { string_to_int(&pod_identifier), )), meta_schema: get_meta_schema(), + prod: prod.to_owned(), + enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), + tenants: tenants.to_owned(), })) .wrap( actix_web::middleware::DefaultHeaders::new() @@ -100,18 +115,40 @@ async fn main() -> Result<()> { get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), ) /***************************** V1 Routes *****************************/ - .service(scope("/context").service(context::endpoints())) - .service(scope("/dimension").service(dimension::endpoints())) - .service(scope("/default-config").service(default_config::endpoints())) + .service( + scope("/context") + .wrap(AppExecutionScope::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScope::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScope::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) .service( scope("/config") .wrap(AuditHeader::new(TableName::Contexts)) + .wrap(AppExecutionScope::new(AppScope::CAC)) .service(config::endpoints()), ) - .service(scope("/audit").service(audit_log::endpoints())) - .service(external::endpoints(experiments::endpoints(scope( - "/experiments", - )))) + .service( + scope("/audit") + .wrap(AppExecutionScope::new(AppScope::CAC)) + .service(audit_log::endpoints()) + ) + .service( + external::endpoints( + experiments::endpoints( + scope("/experiments") + ) + ) + .wrap(AppExecutionScope::new(AppScope::EXPERIMENTATION)) + ) }) .bind(("0.0.0.0", 8080))? .workers(5) @@ -120,4 +157,4 @@ async fn main() -> Result<()> { )) .run() .await -} +} \ No newline at end of file diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 3dd5dd286..9fd71cbf4 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -14,7 +14,7 @@ actix-web = "4.0.0" # To help generate snowflake ids rs-snowflake = "0.6.0" # To help with generating uuids -uuid = {version = "^0.8", features = ["v4","serde"]} +uuid = {version = "1.3.4", features = ["v4", "serde"]} # To serialize and deserialize objects from json serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} @@ -32,4 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 2532a5cdc..a421276f0 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -30,12 +30,12 @@ use super::{ ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextMoveReq, ContextPutReq, ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, - RampRequest, Variant, + RampRequest, Variant, AuditQueryFilters }, }; use crate::{ - db::models::{Experiment, ExperimentStatusType}, + db::models::{Experiment, ExperimentStatusType, EventLog}, db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, }; @@ -44,6 +44,7 @@ use serde_json::{json, Map, Value}; pub fn endpoints(scope: Scope) -> Scope { scope .guard(acl([("mjos_manager".into(), "RW".into())])) + .service(get_audit_logs) .service(create) .service(conclude_handler) .service(list_experiments) @@ -673,3 +674,53 @@ async fn update_overrides( return Ok(Json(ExperimentResponse::from(updated_experiment))); } + +#[get("/audit")] +async fn get_audit_logs( + filters: Query, + db_conn: DbConnection, +) -> app::Result { + let DbConnection(mut conn) = db_conn; + + let query_builder = |filters: &AuditQueryFilters| { + let mut builder = event_log::event_log.into_boxed(); + if let Some(tables) = filters.table.clone() { + builder = builder.filter(event_log::table_name.eq_any(tables.0)); + } + if let Some(actions) = filters.action.clone() { + builder = builder.filter(event_log::action.eq_any(actions.0)); + } + if let Some(username) = filters.username.clone() { + builder = builder.filter(event_log::user_name.eq(username)); + } + let now = Utc::now().naive_utc(); + builder + .filter( + event_log::timestamp + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(event_log::timestamp.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(event_log::timestamp.desc()) + .limit(limit) + .offset(offset); + + let log_count: i64 = count_query.count().get_result(&mut conn)?; + + let logs: Vec = query.load(&mut conn)?; + + let total_pages = (log_count as f64 / limit as f64).ceil() as i64; + + Ok(HttpResponse::Ok().json(json!({ + "total_items": log_count, + "total_pages": total_pages, + "data": logs + }))) +} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 9116d92a2..27b352a43 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc, NaiveDateTime}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use service_utils::helpers::deserialize_stringified_list; @@ -164,7 +164,26 @@ pub struct OverrideKeysUpdateRequest { pub override_keys: Vec, pub variants: Vec, } + #[derive(Deserialize, Serialize, Clone)] pub struct ContextMoveReq { pub context: serde_json::Map, } + +/*********** List Audit API Filter Type **************/ + +#[derive(Deserialize, Debug, Clone)] +pub struct StringArgs( + #[serde(deserialize_with = "deserialize_stringified_list")] pub Vec, +); + +#[derive(Debug, Clone, Deserialize)] +pub struct AuditQueryFilters { + pub from_date: Option, + pub to_date: Option, + pub table: Option, + pub action: Option, + pub username: Option, + pub count: Option, + pub page: Option, +} \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index bef3cd9bc..a3767b208 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -1,5 +1,5 @@ use crate::db::schema::*; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::{Insertable, Queryable, QueryableByName, Selectable}; use serde::{Deserialize, Serialize}; @@ -36,4 +36,19 @@ pub struct Experiment { pub chosen_variant: Option, } -pub type Experiments = Vec; \ No newline at end of file +pub type Experiments = Vec; + +#[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(table_name = event_log)] +#[diesel(primary_key(id))] +pub struct EventLog { + pub id: uuid::Uuid, + pub table_name: String, + pub user_name: String, + pub timestamp: NaiveDateTime, + pub action: String, + pub original_data: Option, + pub new_data: Option, + pub query: String, +} \ No newline at end of file diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 5a33b67fa..6e86bbc8c 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -11,6 +11,7 @@ dotenv = "0.15.0" # Https server framework actix = "0.13.0" actix-web = "4.0.0" +futures-util = "0.3.28" # To help generate snowflake ids rs-snowflake = "0.6.0" #ORM @@ -26,3 +27,4 @@ log = "^0.4" serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} derive_more = "^0.99" +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file diff --git a/crates/service-utils/src/errors/types.rs b/crates/service-utils/src/errors/types.rs index b1a8fb4d3..e08981939 100644 --- a/crates/service-utils/src/errors/types.rs +++ b/crates/service-utils/src/errors/types.rs @@ -64,6 +64,7 @@ impl err::Error for Error {} impl From for Error { fn from(value: diesel::result::Error) -> Self { + log::error!("{}", value); match value { diesel::result::Error::InvalidCString(e) => { Self::ConnectionFailed("DATABASE".into(), e.to_string()) @@ -147,4 +148,4 @@ impl error::ResponseError for Error { Error::Generic(server_error) => server_error.error_response(), } } -} +} \ No newline at end of file diff --git a/crates/service-utils/src/lib.rs b/crates/service-utils/src/lib.rs index f32de15cb..b8d22b7bf 100644 --- a/crates/service-utils/src/lib.rs +++ b/crates/service-utils/src/lib.rs @@ -1,6 +1,7 @@ pub mod aws; pub mod db; +pub mod errors; pub mod helpers; +pub mod middlewares; pub mod service; -pub mod errors; -pub mod types; \ No newline at end of file +pub mod types; diff --git a/crates/service-utils/src/middlewares/app_scope.rs b/crates/service-utils/src/middlewares/app_scope.rs new file mode 100644 index 000000000..60e528abc --- /dev/null +++ b/crates/service-utils/src/middlewares/app_scope.rs @@ -0,0 +1,66 @@ +use std::future::{ready, Ready}; + +use crate::service::types::AppScope; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + Error, HttpMessage, +}; +use futures_util::future::LocalBoxFuture; + +pub struct AppExecutionScope { + scope: AppScope, +} + +impl AppExecutionScope { + pub fn new(scope: AppScope) -> Self { + AppExecutionScope { scope } + } +} + +impl Transform for AppExecutionScope +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = AppExecutionScopeMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AppExecutionScopeMiddleware { + service, + scope: self.scope, + })) + } +} + +pub struct AppExecutionScopeMiddleware { + service: S, + scope: AppScope, +} + +impl Service for AppExecutionScopeMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + req.extensions_mut().insert(self.scope); + let future = self.service.call(req); + + Box::pin(async move { + let res = future.await?; + Ok(res) + }) + } +} diff --git a/crates/service-utils/src/middlewares/mod.rs b/crates/service-utils/src/middlewares/mod.rs new file mode 100644 index 000000000..4f605bffa --- /dev/null +++ b/crates/service-utils/src/middlewares/mod.rs @@ -0,0 +1 @@ +pub mod app_scope; diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index c5cbdf17c..cf27db2cf 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -4,13 +4,17 @@ use diesel::{ }; use jsonschema::JSONSchema; -use std::future::{ready, Ready}; +use std::{collections::HashSet, future::{ready, Ready}}; -use actix_web::{error, web::Data, Error, FromRequest}; +use actix_web::{ + error, web::Data, Error, FromRequest, HttpMessage, +}; use snowflake::SnowflakeIdGenerator; use std::sync::Mutex; +use dashboard_auth::types::Tenant; + pub struct ExperimentationFlags { pub allow_same_keys_overlapping_ctx: bool, pub allow_diff_keys_overlapping_ctx: bool, @@ -19,6 +23,8 @@ pub struct ExperimentationFlags { pub struct AppState { pub cac_host: String, + pub prod: bool, + pub tenants: HashSet, pub cac_version: String, pub admin_token: String, pub db_pool: Pool>, @@ -26,6 +32,22 @@ pub struct AppState { pub meta_schema: JSONSchema, pub experimentation_flags: ExperimentationFlags, pub snowflake_generator: Mutex, + pub enable_tenant_and_scope: bool, +} + +#[derive(Copy, Clone, Debug)] +pub enum AppScope { + CAC, + EXPERIMENTATION, +} + +impl ToString for AppScope { + fn to_string(&self) -> String { + match self { + AppScope::CAC => String::from("cac"), + AppScope::EXPERIMENTATION => String::from("experimentation"), + } + } } pub type DBConnection = PooledConnection>; @@ -57,3 +79,53 @@ impl FromRequest for DbConnection { ready(result) } } + +pub struct TenantNScope(pub String); +impl FromRequest for TenantNScope { + type Error = Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + payload: &mut actix_web::dev::Payload, + ) -> Self::Future { + let app_state = match req.app_data::>() { + Some(state) => state, + None => { + log::info!("Unable to get app_date from request"); + return ready(Err(error::ErrorInternalServerError(""))); + } + }; + + if !app_state.enable_tenant_and_scope { + let default_tenant = match app_state.prod { + true => "cac_v1".to_string(), + false => "public".to_string() + }; + + return ready(Ok(TenantNScope(default_tenant))); + } + + // extract headers for tenant id + let tenant = Tenant::from_request(req, payload) + .into_inner() + .map_or(None, |value: Tenant| Some(value.as_str().to_string())); + + let scope = req + .extensions() + .get::() + .and_then(|app_scope| Some(app_scope.to_string())); + + let tenant_n_scope:Result = match (tenant, scope) { + (Some(t), Some(s)) if app_state.tenants.contains(&t) => Ok(TenantNScope(format!("{t}_{s}"))), + (Some(_), Some(_)) => Err(error::ErrorBadRequest("invalid x-tenant value")), + (None, _) => Err(error::ErrorBadRequest("x-tenant not set")), + (_, None) => { + log::error!("AppScope not set for the request!"); + Err(error::ErrorInternalServerError("something went wrong")) + } + }; + + ready(tenant_n_scope) + } +} \ No newline at end of file diff --git a/crates/service-utils/src/types.rs b/crates/service-utils/src/types.rs index f8e7a8b76..448819675 100644 --- a/crates/service-utils/src/types.rs +++ b/crates/service-utils/src/types.rs @@ -1,2 +1 @@ - pub type Result = core::result::Result; diff --git a/makefile b/makefile index 730764b58..4d384df42 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,6 @@ IMAGE_NAME ?= context-aware-config DOCKER_DNS ?= localhost +TENANT ?= dev SHELL := /usr/bin/env bash .PHONY: @@ -49,6 +50,9 @@ migration: make cac-migration make exp-migration +tenant: + grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/create-tenant.sh $(TENANT) + validate-aws-connection: aws --endpoint-url=http://$(DOCKER_DNS):4566 --region=ap-south-1 sts get-caller-identity @@ -82,14 +86,18 @@ cac: run: -make kill cargo build --color always - make setup + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + # make setup make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") npm ci --loglevel=error - cargo test + make setup make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ do echo "ci-test: waiting for bootup..." && sleep 4; \ diff --git a/scripts/cac-init.sql b/scripts/cac-init.sql new file mode 100644 index 000000000..1f4198699 --- /dev/null +++ b/scripts/cac-init.sql @@ -0,0 +1,221 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- {{schema}}.event_log definition + +-- Drop table + +-- DROP TABLE {{schema}}.event_log; + +CREATE TABLE {{schema}}.event_log ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + table_name text NOT NULL, + user_name text NOT NULL, + "timestamp" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "action" text NOT NULL, + original_data json NULL, + new_data json NULL, + query text NOT NULL, + CONSTRAINT event_log_pkey PRIMARY KEY (id, "timestamp") +) +PARTITION BY RANGE ("timestamp"); +CREATE INDEX event_log_action_index ON ONLY {{schema}}.event_log USING btree (action) INCLUDE ("timestamp", table_name); +CREATE INDEX event_log_table_name_index ON ONLY {{schema}}.event_log USING btree (table_name) INCLUDE (action, "timestamp"); +CREATE INDEX event_log_timestamp_index ON ONLY {{schema}}.event_log USING btree ("timestamp") INCLUDE (action, table_name); + + +-- {{schema}}.event_log_y2023m08 definition + +CREATE TABLE {{schema}}.event_log_y2023m08 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-08-01 00:00:00') TO ('2023-09-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m09 definition + +CREATE TABLE {{schema}}.event_log_y2023m09 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-09-01 00:00:00') TO ('2023-10-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m10 definition + +CREATE TABLE {{schema}}.event_log_y2023m10 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-10-01 00:00:00') TO ('2023-11-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m11 definition + +CREATE TABLE {{schema}}.event_log_y2023m11 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-11-01 00:00:00') TO ('2023-12-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m12 definition + +CREATE TABLE {{schema}}.event_log_y2023m12 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-12-01 00:00:00') TO ('2024-01-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m01 definition + +CREATE TABLE {{schema}}.event_log_y2024m01 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-01-01 00:00:00') TO ('2024-02-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m02 definition + +CREATE TABLE {{schema}}.event_log_y2024m02 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-02-01 00:00:00') TO ('2024-03-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m03 definition + +CREATE TABLE {{schema}}.event_log_y2024m03 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-03-01 00:00:00') TO ('2024-04-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m04 definition + +CREATE TABLE {{schema}}.event_log_y2024m04 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-04-01 00:00:00') TO ('2024-05-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m05 definition + +CREATE TABLE {{schema}}.event_log_y2024m05 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-05-01 00:00:00') TO ('2024-06-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m06 definition + +CREATE TABLE {{schema}}.event_log_y2024m06 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-06-01 00:00:00') TO ('2024-07-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m07 definition + +CREATE TABLE {{schema}}.event_log_y2024m07 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-07-01 00:00:00') TO ('2024-08-01 00:00:00'); + + +CREATE OR REPLACE FUNCTION {{schema}}.event_logger() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +DECLARE + old_data json; + new_data json; +BEGIN + IF (TG_OP = 'UPDATE') THEN + old_data := row_to_json(OLD); + new_data := row_to_json(NEW); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, original_data, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + new_data, + current_query() + ); + + ELSIF (TG_OP = 'DELETE') THEN + old_data := row_to_json(OLD); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, original_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + current_query() + ); + + ELSIF (TG_OP = 'INSERT') THEN + new_data = row_to_json(NEW); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + new_data, + current_query() + ); + END IF; + + RETURN NULL; +END; +$function$ +; + +-- {{schema}}.contexts definition + +-- Drop table + +-- DROP TABLE {{schema}}.contexts; + +CREATE TABLE {{schema}}.contexts ( + id varchar NOT NULL, + value json NOT NULL, + override_id varchar NOT NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by varchar NOT NULL, + priority int4 NOT NULL DEFAULT 1, + override json NOT NULL DEFAULT '{}'::json, + CONSTRAINT contexts_pkey PRIMARY KEY (id) +); + +-- Table Triggers + +create trigger contexts_audit after +insert + or +delete + or +update + on + {{schema}}.contexts for each row execute function {{schema}}.event_logger(); + + +-- {{schema}}.default_configs definition + +-- Drop table + +-- DROP TABLE {{schema}}.default_configs; + +CREATE TABLE {{schema}}.default_configs ( + "key" varchar NOT NULL, + value json NOT NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by varchar NOT NULL, + "schema" json NOT NULL DEFAULT '{}'::json, + CONSTRAINT default_configs_pkey PRIMARY KEY (key) +); + +-- Table Triggers + +create trigger default_configs_audit after +insert + or +delete + or +update + on + {{schema}}.default_configs for each row execute function {{schema}}.event_logger(); + + +-- {{schema}}.dimensions definition + +-- Drop table + +-- DROP TABLE {{schema}}.dimensions; + +CREATE TABLE {{schema}}.dimensions ( + dimension varchar NOT NULL, + priority int4 NOT NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by varchar NOT NULL, + "schema" json NOT NULL DEFAULT '{}'::json, + CONSTRAINT dimensions_pkey PRIMARY KEY (dimension) +); + +-- Table Triggers + +create trigger dimensions_audit after +insert + or +delete + or +update + on + {{schema}}.dimensions for each row execute function {{schema}}.event_logger(); \ No newline at end of file diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh new file mode 100755 index 000000000..c638f50bb --- /dev/null +++ b/scripts/create-tenant.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +HOST="localhost" +PORT="5432" +USER="postgres" +PASSWORD="docker" +DATABASE="config" + +TENANT=$1 +DB_URL=$2 + +echo "Tenant ID ==> $TENANT" +echo "DB URL ==> $DB_URL" + +# Creating schemas +SCHEMA1="${TENANT}_cac" +SCHEMA2="${TENANT}_experimentation" + +echo "Creating Schemas ==> $SCHEMA1, $SCHEMA2" + +psql "$DB_URL" -c "CREATE SCHEMA ${SCHEMA1}" +psql "$DB_URL" -c "CREATE SCHEMA ${SCHEMA2}" + +# Running migrations in created schemas +sed "s/{{schema}}/${SCHEMA1}/g" "$PWD/scripts/cac-init.sql" > $PWD/scripts/cac-init-with-schema.sql +sed "s/{{schema}}/${SCHEMA2}/g" "$PWD/scripts/experimentation-init.sql" > $PWD/scripts/experimentation-init-with-schema.sql + +psql "$DB_URL" -f "$PWD/scripts/cac-init-with-schema.sql" +psql "$DB_URL" -f "$PWD/scripts/experimentation-init-with-schema.sql" \ No newline at end of file diff --git a/scripts/experimentation-init.sql b/scripts/experimentation-init.sql new file mode 100644 index 000000000..fd7d4b4b1 --- /dev/null +++ b/scripts/experimentation-init.sql @@ -0,0 +1,187 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- DROP TYPE {{schema}}.experiment_status_type; + +CREATE TYPE {{schema}}.experiment_status_type AS ENUM ( + 'CREATED', + 'CONCLUDED', + 'INPROGRESS'); + +-- DROP DOMAIN {{schema}}.not_null_text; + +CREATE DOMAIN {{schema}}.not_null_text AS text not null; + + +-- {{schema}}.event_log definition + +-- Drop table + +-- DROP TABLE {{schema}}.event_log; + +CREATE TABLE {{schema}}.event_log ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + table_name text NOT NULL, + user_name text NOT NULL, + "timestamp" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "action" text NOT NULL, + original_data json NULL, + new_data json NULL, + query text NOT NULL, + CONSTRAINT event_log_pkey PRIMARY KEY (id, "timestamp") +) +PARTITION BY RANGE ("timestamp"); +CREATE INDEX event_log_action_index ON ONLY {{schema}}.event_log USING btree (action) INCLUDE ("timestamp", table_name); +CREATE INDEX event_log_table_name_index ON ONLY {{schema}}.event_log USING btree (table_name) INCLUDE (action, "timestamp"); +CREATE INDEX event_log_timestamp_index ON ONLY {{schema}}.event_log USING btree ("timestamp") INCLUDE (action, table_name); + +CREATE OR REPLACE FUNCTION {{schema}}.event_logger() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +DECLARE + old_data json; + new_data json; +BEGIN + IF (TG_OP = 'UPDATE') THEN + old_data := row_to_json(OLD); + new_data := row_to_json(NEW); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, original_data, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + new_data, + current_query() + ); + + ELSIF (TG_OP = 'DELETE') THEN + old_data := row_to_json(OLD); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, original_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + old_data, + current_query() + ); + + ELSIF (TG_OP = 'INSERT') THEN + new_data = row_to_json(NEW); + + INSERT INTO {{schema}}.event_log + (table_name, user_name, action, new_data, query) + VALUES ( + TG_TABLE_NAME::TEXT, + session_user::TEXT, + TG_OP, + new_data, + current_query() + ); + END IF; + + RETURN NULL; +END; +$function$ +; + +-- {{schema}}.experiments definition + +-- Drop table + +-- DROP TABLE {{schema}}.experiments; + +CREATE TABLE {{schema}}.experiments ( + id int8 NOT NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by text NOT NULL, + last_modified timestamptz NOT NULL DEFAULT now(), + "name" text NOT NULL, + override_keys {{schema}}.not_null_text [] NOT NULL, + status {{schema}}.experiment_status_type NOT NULL, + traffic_percentage int4 NOT NULL, + context json NOT NULL, + variants json NOT NULL, + last_modified_by text NOT NULL DEFAULT 'Null'::text, + chosen_variant text NULL, + CONSTRAINT experiments_pkey PRIMARY KEY (id), + CONSTRAINT experiments_traffic_percentage_check CHECK ((traffic_percentage >= 0)) +); +CREATE INDEX experiment_created_date_index ON {{schema}}.experiments USING btree (created_at) INCLUDE (id); +CREATE INDEX experiment_last_modified_index ON {{schema}}.experiments USING btree (last_modified) INCLUDE (id, created_at); +CREATE INDEX experiment_status_index ON {{schema}}.experiments USING btree (status) INCLUDE (created_at, last_modified); + +-- Table Triggers + +create trigger experiments_audit after +insert + or +delete + or +update + on + {{schema}}.experiments for each row execute function {{schema}}.event_logger(); + + +-- {{schema}}.event_log_y2023m08 definition + +CREATE TABLE {{schema}}.event_log_y2023m08 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-08-01 00:00:00') TO ('2023-09-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m09 definition + +CREATE TABLE {{schema}}.event_log_y2023m09 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-09-01 00:00:00') TO ('2023-10-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m10 definition + +CREATE TABLE {{schema}}.event_log_y2023m10 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-10-01 00:00:00') TO ('2023-11-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m11 definition + +CREATE TABLE {{schema}}.event_log_y2023m11 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-11-01 00:00:00') TO ('2023-12-01 00:00:00'); + + +-- {{schema}}.event_log_y2023m12 definition + +CREATE TABLE {{schema}}.event_log_y2023m12 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-12-01 00:00:00') TO ('2024-01-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m01 definition + +CREATE TABLE {{schema}}.event_log_y2024m01 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-01-01 00:00:00') TO ('2024-02-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m02 definition + +CREATE TABLE {{schema}}.event_log_y2024m02 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-02-01 00:00:00') TO ('2024-03-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m03 definition + +CREATE TABLE {{schema}}.event_log_y2024m03 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-03-01 00:00:00') TO ('2024-04-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m04 definition + +CREATE TABLE {{schema}}.event_log_y2024m04 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-04-01 00:00:00') TO ('2024-05-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m05 definition + +CREATE TABLE {{schema}}.event_log_y2024m05 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-05-01 00:00:00') TO ('2024-06-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m06 definition + +CREATE TABLE {{schema}}.event_log_y2024m06 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-06-01 00:00:00') TO ('2024-07-01 00:00:00'); + + +-- {{schema}}.event_log_y2024m07 definition + +CREATE TABLE {{schema}}.event_log_y2024m07 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-07-01 00:00:00') TO ('2024-08-01 00:00:00'); \ No newline at end of file From 44e9b478faf5e92125a5a3d2cbe9736eebace217 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Wed, 11 Oct 2023 17:12:31 +0530 Subject: [PATCH 160/352] feat: added multi-tenant support --- .env.example | 1 - Cargo.lock | 96 ++++---- .../src/api/context/handlers.rs | 54 ++--- .../src/api/default_config/handlers.rs | 16 +- .../src/api/dimension/handlers.rs | 15 +- .../src/api/dimension/utils.rs | 4 +- crates/context-aware-config/src/main.rs | 50 ++-- .../src/middlewares/audit_response_header.rs | 67 +++--- .../src/api/experiments/handlers.rs | 16 +- crates/service-utils/Cargo.toml | 4 + crates/service-utils/src/db/mod.rs | 1 + .../service-utils/src/db/pgschema_manager.rs | 76 ++++++ crates/service-utils/src/db/utils.rs | 40 +++- .../src/middlewares/app_scope.rs | 30 +-- crates/service-utils/src/middlewares/mod.rs | 1 + .../service-utils/src/middlewares/tenant.rs | 84 +++++++ crates/service-utils/src/service/types.rs | 191 +++++++++------ makefile | 7 + scripts/cac-init.sql | 221 ------------------ scripts/create-tenant.sh | 29 +-- scripts/experimentation-init.sql | 187 --------------- scripts/legacy-db-setup.sh | 17 ++ 22 files changed, 516 insertions(+), 691 deletions(-) create mode 100644 crates/service-utils/src/db/pgschema_manager.rs create mode 100644 crates/service-utils/src/middlewares/tenant.rs delete mode 100644 scripts/cac-init.sql delete mode 100644 scripts/experimentation-init.sql create mode 100755 scripts/legacy-db-setup.sh diff --git a/.env.example b/.env.example index ab355761a..973544510 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,5 @@ AUTH_EXCLUSION_LIST="GET /context/list,GET /context/{ctx_id},GET /config/resolve DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 -PROD=false ENABLE_TENANT_AND_SCOPE=false TENANTS=tenant1,tenant2,tenant3 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 799396712..d4aee0713 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,7 +727,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.9.24", + "reqwest 0.11.20", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -912,15 +912,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "cxx" version = "1.0.94" @@ -1676,19 +1667,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.17.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes 0.4.12", - "ct-logs", - "futures 0.1.31", - "hyper 0.12.36", + "futures-util", + "http 0.2.9", + "hyper 0.14.26", "rustls", - "tokio-io", + "tokio 1.29.1", "tokio-rustls", - "webpki", - "webpki-roots", ] [[package]] @@ -2738,13 +2726,11 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", - "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", - "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2752,12 +2738,10 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", - "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", - "webpki-roots", "winreg 0.6.2", ] @@ -2776,6 +2760,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", + "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2785,16 +2770,20 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", + "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg 0.50.0", ] @@ -2942,15 +2931,33 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "base64 0.10.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2997,9 +3004,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3119,12 +3126,14 @@ version = "0.6.0" dependencies = [ "actix", "actix-web", + "anyhow", "base64 0.21.2", "bytes 1.4.0", "dashboard-auth", "derive_more", "diesel", "dotenv", + "env_logger 0.8.4", "futures-util", "jsonschema", "log", @@ -3134,6 +3143,8 @@ dependencies = [ "rusoto_signature", "serde", "serde_json", + "strum", + "strum_macros", "urlencoding", ] @@ -3609,16 +3620,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.10.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", "rustls", - "tokio-io", - "webpki", + "tokio 1.29.1", ] [[package]] @@ -4090,24 +4097,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.17.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index bc8cd8b34..90cb8a002 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -20,7 +20,7 @@ use actix_web::{ delete, error::{self, ErrorInternalServerError, ErrorNotFound}, get, put, - web::{self, Data, Path}, + web::{self, Path}, HttpResponse, Responder, Result, Scope, }; use anyhow::anyhow; @@ -33,7 +33,7 @@ use diesel::{ Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; use serde_json::{from_value, json, Map, Value, Value::Null}; -use service_utils::{helpers::ToActixErr, service::types::AppState}; +use service_utils::{helpers::ToActixErr, service::types::DbConnection}; use jsonschema::{Draft, JSONSchema, ValidationError}; pub fn endpoints() -> Scope { @@ -269,14 +269,10 @@ fn put( #[put("")] async fn put_handler( req: web::Json, - state: Data, user: User, + mut db_conn: DbConnection, ) -> actix_web::Result> { - let conn = &mut state - .db_pool - .get() - .map_err_to_internal_server("unable to get db connection from pool", "")?; - put(req, &user, conn, false) + put(req, &user, &mut db_conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("context put failed with error: {:?}", e); @@ -364,17 +360,12 @@ fn r#move( #[put("/move/{ctx_id}")] async fn move_handler( - state: Data, path: Path, req: web::Json, user: User, + mut db_conn: DbConnection, ) -> actix_web::Result> { - let conn = &mut state - .db_pool - .get() - .map_err_to_internal_server("unable to get db connection from pool", "")?; - - r#move(path.into_inner(), req, &user, conn, false) + r#move(path.into_inner(), req, &user, &mut db_conn, false) .map(|resp| web::Json(resp)) .map_err(|e| { log::info!("move api failed with error: {:?}", e); @@ -385,18 +376,12 @@ async fn move_handler( #[get("/{ctx_id}")] async fn get_context( path: web::Path, - state: Data, + db_conn: DbConnection, ) -> Result { use crate::db::schema::contexts::dsl::*; let ctx_id = path.into_inner(); - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - log::info!("Unable to get db connection from pool, error: {e}"); - return Err(error::ErrorInternalServerError("")); - } - }; + let DbConnection(mut conn) = db_conn; let result: QueryResult> = contexts.filter(id.eq(ctx_id)).load(&mut conn); @@ -418,14 +403,10 @@ async fn get_context( #[get("/list")] async fn list_contexts( qparams: web::Query, - state: Data, + db_conn: DbConnection, ) -> Result { use crate::db::schema::contexts::dsl::*; - - let mut conn = state - .db_pool - .get() - .map_err_to_internal_server("Unable to get db connection from pool", Null)?; + let DbConnection(mut conn) = db_conn; let PaginationParams { page: opt_page, @@ -453,15 +434,13 @@ async fn list_contexts( #[delete("/{ctx_id}")] async fn delete_context( - state: Data, path: Path, user: User, + db_conn: DbConnection, ) -> actix_web::Result { use contexts::dsl; - let mut conn = state - .db_pool - .get() - .map_err_to_internal_server("Unable to get db connection from pool", "")?; + let DbConnection(mut conn) = db_conn; + let ctx_id = path.into_inner(); let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(&mut conn); @@ -481,14 +460,11 @@ async fn delete_context( #[put("/bulk-operations")] async fn bulk_operations( reqs: web::Json>, - state: Data, user: User, + db_conn: DbConnection, ) -> actix_web::Result>> { use contexts::dsl::contexts; - let mut conn = state - .db_pool - .get() - .map_err_to_internal_server("Unable to get db connection from pool", "")?; + let DbConnection(mut conn) = db_conn; let mut resp = Vec::::new(); let result = conn.transaction::<_, diesel::result::Error, _>(|transaction_conn| { diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index eba094670..614bb067f 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,7 +1,7 @@ use super::types::CreateReq; use crate::{ db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}, - helpers::validate_jsonschema + helpers::validate_jsonschema, }; use actix_web::{ put, @@ -13,7 +13,7 @@ use dashboard_auth::{middleware::acl, types::User}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; -use service_utils::service::types::AppState; +use service_utils::service::types::{AppState, DbConnection}; pub fn endpoints() -> Scope { Scope::new("") @@ -27,7 +27,9 @@ async fn create( key: web::Path, request: web::Json, user: User, + db_conn: DbConnection, ) -> HttpResponse { + let DbConnection(mut conn) = db_conn; let req = request.into_inner(); let schema = Value::Object(req.schema); if let Err(e) = validate_jsonschema(&state.default_config_validation_schema, &schema) @@ -62,14 +64,6 @@ async fn create( created_at: Utc::now(), }; - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - log::info!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - }; - let upsert = diesel::insert_into(default_configs) .values(&new_default_config) .execute(&mut conn); @@ -84,4 +78,4 @@ async fn create( .body("Failed to create DefaultConfig"); } } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 6d45a2107..568c89df0 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -12,7 +12,7 @@ use chrono::Utc; use dashboard_auth::{middleware::acl, types::User}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; -use service_utils::service::types::AppState; +use service_utils::service::types::{AppState, DbConnection}; pub fn endpoints() -> Scope { Scope::new("") @@ -25,8 +25,11 @@ async fn create( state: Data, req: web::Json, user: User, + db_conn: DbConnection, ) -> HttpResponse { //TODO move this to the type itself rather than special if check + let DbConnection(mut conn) = db_conn; + if req.priority <= 0 { return HttpResponse::BadRequest().body("Priority should be greater than 0"); } @@ -55,14 +58,6 @@ async fn create( created_at: Utc::now(), }; - let mut conn = match state.db_pool.get() { - Ok(conn) => conn, - Err(e) => { - log::info!("unable to get db connection from pool, error: {e}"); - return HttpResponse::InternalServerError().finish(); - } - }; - let upsert = diesel::insert_into(dimensions) .values(&new_dimension) .on_conflict(dimension) @@ -81,4 +76,4 @@ async fn create( .body("Failed to create/update dimension\n"); } } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context-aware-config/src/api/dimension/utils.rs index 790889cd9..f62a43c47 100644 --- a/crates/context-aware-config/src/api/dimension/utils.rs +++ b/crates/context-aware-config/src/api/dimension/utils.rs @@ -3,10 +3,10 @@ use std::collections::HashMap; use crate::db::{models::Dimension, schema::dimensions::dsl::*}; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; -use service_utils::{service::types::DBConnection}; +use diesel::{ r2d2::{ConnectionManager, PooledConnection}, PgConnection }; pub fn get_all_dimension_schema_map( - conn: &mut DBConnection, + conn: &mut PooledConnection>, ) -> anyhow::Result> { let dimensions_vec = dimensions.load::(conn)?; diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 1fc2d7793..9e7276ed9 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -12,7 +12,7 @@ use dotenv; use experimentation_platform::api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; use logger::{init_log_subscriber, CustomRootSpanBuilder}; -use std::{env, io::Result, collections::HashSet}; +use std::{collections::HashSet, env, io::Result}; use tracing::{span, Level}; use snowflake::SnowflakeIdGenerator; @@ -20,10 +20,14 @@ use std::{sync::Mutex, time::Duration}; use tracing_actix_web::TracingLogger; use service_utils::{ - db::utils::get_pool, + db::pgschema_manager::PgSchemaManager, + db::utils::init_pool_manager, helpers::{get_from_env_unsafe, get_pod_info}, - middlewares::app_scope::AppExecutionScope, - service::types::{AppScope, AppState, ExperimentationFlags}, + middlewares::{ + app_scope::AppExecutionScopeMiddlewareFactory, + tenant::TenantMiddlewareFactory, + }, + service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, }; #[actix_web::main] @@ -39,13 +43,13 @@ async fn main() -> Result<()> { deployment_id = deployment_id ); let _span_entered = cac_span.enter(); - let pool = get_pool().await; let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let max_pool_size = get_from_env_unsafe("MAX_DB_CONNECTION_POOL_SIZE").unwrap_or(3); - let prod: bool = get_from_env_unsafe("PROD").expect("PROD is not set"); + let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") .expect("ENABLE_TENANT_AND_SCOPE is not set"); let tenants: HashSet = get_from_env_unsafe::("TENANTS") @@ -59,6 +63,15 @@ async fn main() -> Result<()> { .map(|i| (i as i32) & rand::random::()) .fold(0, i32::wrapping_add) }; + + let schema_manager: PgSchemaManager = init_pool_manager( + tenants.clone(), + enable_tenant_and_scope, + app_env, + max_pool_size, + ) + .await; + /****** EXPERIMENTATION PLATFORM ENVs *********/ let allow_same_keys_overlapping_ctx: bool = @@ -77,10 +90,11 @@ async fn main() -> Result<()> { App::new() .wrap(DashboardAuth::default()) .wrap(middlewares::cors()) + .wrap(TenantMiddlewareFactory) .wrap(logger::GoldenSignalFactory) .wrap(TracingLogger::::new()) .app_data(Data::new(AppState { - db_pool: pool.clone(), + db_pool: schema_manager.clone(), default_config_validation_schema: get_default_config_validation_schema(), admin_token: admin_token.to_owned(), cac_host: cac_host.to_owned(), @@ -100,7 +114,7 @@ async fn main() -> Result<()> { string_to_int(&pod_identifier), )), meta_schema: get_meta_schema(), - prod: prod.to_owned(), + app_env: app_env.to_owned(), enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), tenants: tenants.to_owned(), })) @@ -117,37 +131,33 @@ async fn main() -> Result<()> { /***************************** V1 Routes *****************************/ .service( scope("/context") - .wrap(AppExecutionScope::new(AppScope::CAC)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(context::endpoints()), ) .service( scope("/dimension") - .wrap(AppExecutionScope::new(AppScope::CAC)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(dimension::endpoints()), ) .service( scope("/default-config") - .wrap(AppExecutionScope::new(AppScope::CAC)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(default_config::endpoints()), ) .service( scope("/config") .wrap(AuditHeader::new(TableName::Contexts)) - .wrap(AppExecutionScope::new(AppScope::CAC)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(config::endpoints()), ) .service( scope("/audit") - .wrap(AppExecutionScope::new(AppScope::CAC)) - .service(audit_log::endpoints()) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), ) .service( - external::endpoints( - experiments::endpoints( - scope("/experiments") - ) - ) - .wrap(AppExecutionScope::new(AppScope::EXPERIMENTATION)) + external::endpoints(experiments::endpoints(scope("/experiments"))) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION)), ) }) .bind(("0.0.0.0", 8080))? diff --git a/crates/context-aware-config/src/middlewares/audit_response_header.rs b/crates/context-aware-config/src/middlewares/audit_response_header.rs index a6efb9309..34ef8ed58 100644 --- a/crates/context-aware-config/src/middlewares/audit_response_header.rs +++ b/crates/context-aware-config/src/middlewares/audit_response_header.rs @@ -3,15 +3,15 @@ use std::future::{ready, Ready}; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, http::header::{HeaderName, HeaderValue}, - web::Data, - Error, + Error }; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use futures_util::future::LocalBoxFuture; -use service_utils::service::types::AppState; +use service_utils::service::types::DbConnection; use crate::db::schema::event_log::dsl as event_log; use uuid::Uuid; +use std::rc::Rc; #[derive(Clone, Copy, Debug, strum_macros::Display)] #[strum(serialize_all = "snake_case")] @@ -35,7 +35,7 @@ impl AuditHeader { impl Transform for AuditHeader where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, B: 'static, { @@ -47,20 +47,20 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(AuditHeaderMiddleware { - service, + service: Rc::new(service), table_name: self.table_name, })) } } pub struct AuditHeaderMiddleware { - service: S, + service: Rc, table_name: TableName, } impl Service for AuditHeaderMiddleware where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, B: 'static, { @@ -70,41 +70,30 @@ where forward_ready!(service); - fn call(&self, req: ServiceRequest) -> Self::Future { - let app_state = req.app_data::>(); - let mut db_conn_option = None; - - if let Some(app_data) = app_state { - match app_data.db_pool.get() { - Ok(conn) => db_conn_option = Some(conn), - Err(_) => log::error!("Failed to get connection"), - } - } else { - log::error!("App State not available"); - } - - let fut = self.service.call(req); + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); let table_name = self.table_name; Box::pin(async move { - let mut res = fut.await?; - - if let Some(mut conn) = db_conn_option { - let uuid = event_log::event_log - .select(event_log::id) - .filter(event_log::table_name.eq(table_name.to_string())) - .order_by(event_log::timestamp.desc()) - .first::(&mut conn); - - if let Ok(uuid) = uuid { - res.headers_mut().insert( - HeaderName::from_static("x-audit-id"), - HeaderValue::from_str(&uuid.to_string()) - .unwrap_or_else(|_| HeaderValue::from_static("invalid")), - ); - } else { - log::error!("Unable to fetch uuid"); - } + let db_conn = req.extract::().await?; + let DbConnection(mut conn) = db_conn; + + let mut res = srv.call(req).await?; + + let uuid = event_log::event_log + .select(event_log::id) + .filter(event_log::table_name.eq(table_name.to_string())) + .order_by(event_log::timestamp.desc()) + .first::(&mut conn); + + if let Ok(uuid) = uuid { + res.headers_mut().insert( + HeaderName::from_static("x-audit-id"), + HeaderValue::from_str(&uuid.to_string()) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), + ); + } else { + log::error!("Unable to fetch uuid"); } Ok(res) }) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index a421276f0..74319e215 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -423,7 +423,7 @@ async fn ramp( req: web::Json, db_conn: DbConnection, user: User, -) -> app::Result> { +) -> app::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); @@ -455,7 +455,7 @@ async fn ramp( possible_fix: "".to_string(), })); } - let new_traffic_percentage = diesel::update(experiments::experiments) + let updated_experiment: Experiment = diesel::update(experiments::experiments) .filter(experiments::id.eq(exp_id)) .set(( experiments::traffic_percentage.eq(req.traffic_percentage as i32), @@ -463,17 +463,9 @@ async fn ramp( experiments::last_modified_by.eq(user.email), experiments::status.eq(ExperimentStatusType::INPROGRESS), )) - .execute(&mut conn)?; + .get_result(&mut conn)?; - if new_traffic_percentage == 0 { - return Err(err::InternalServerErr( - "Failed to update the traffic_percentage".to_string(), - )); - } - return Ok(Json(format!( - "Traffic percentage has been updated for the experiment id : {}", - exp_id - ))); + return Ok(Json(ExperimentResponse::from(updated_experiment))); } #[put("/{id}/overrides")] diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 6e86bbc8c..9edbac676 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -16,6 +16,8 @@ futures-util = "0.3.28" rs-snowflake = "0.6.0" #ORM diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +env_logger = "0.8" +anyhow = "1.0.75" rusoto_kms = "0.48.0" rusoto_signature = "0.48.0" bytes = "1.4.0" @@ -26,5 +28,7 @@ jsonschema = "~0.17" log = "^0.4" serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} +strum_macros = "^0.24" +strum = {version = "^0.24"} derive_more = "^0.99" dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file diff --git a/crates/service-utils/src/db/mod.rs b/crates/service-utils/src/db/mod.rs index b5614dd82..c3e0aebb8 100644 --- a/crates/service-utils/src/db/mod.rs +++ b/crates/service-utils/src/db/mod.rs @@ -1 +1,2 @@ +pub mod pgschema_manager; pub mod utils; diff --git a/crates/service-utils/src/db/pgschema_manager.rs b/crates/service-utils/src/db/pgschema_manager.rs new file mode 100644 index 000000000..2c9039b62 --- /dev/null +++ b/crates/service-utils/src/db/pgschema_manager.rs @@ -0,0 +1,76 @@ +extern crate derive_more; +use derive_more::{Deref, DerefMut, Display}; +use std::collections::HashMap; + +use diesel::{ + r2d2::{ConnectionManager, Pool, PooledConnection}, + PgConnection, +}; +use anyhow::anyhow; + +pub type PgSchemaConnectionPool = Pool>; +pub type PgSchemaConnection = PooledConnection>; + +#[derive(Debug, Display)] +#[display(fmt = "connection config {:?}", self)] +pub struct ConnectionConfig { + name: String, + database_url: String, + schema: String, + count: u32, +} + +impl ConnectionConfig { + pub fn new(name: String, database_url: String, schema: String, count: u32) -> Self { + ConnectionConfig { + name, + database_url, + schema, + count, + } + } + + pub fn conn_url(&self) -> String { + if self.database_url.contains("?") { + format!( + "{}&options=-c%20search_path%3D{},$user,public", + self.database_url, self.schema + ) + } else { + format!( + "{}?options=-c%20search_path%3D{},$user,public", + self.database_url, self.schema + ) + } + } +} + +#[derive(Deref, DerefMut, Clone)] +pub struct PgSchemaManager(HashMap); + +impl From> for PgSchemaManager { + fn from(value: Vec) -> Self { + let mut schema_manager: PgSchemaManager = PgSchemaManager(HashMap::new()); + for config in value.into_iter() { + let manager = ConnectionManager::::new(config.conn_url()); + schema_manager.insert( + config.name.clone(), + Pool::builder() + .max_size(config.count) + .build(manager) + .expect(format!("Invalid config provided, {}", config.name).as_str()), + ); + } + schema_manager + } +} + +impl PgSchemaManager { + pub fn get_conn(&self, name: String) -> anyhow::Result { + let conn = self + .get(&name) // gets the pool for the given namespace + .ok_or_else(|| anyhow!("Invalid connection name provided: {}", name))? + .get()?; // fetches the connection from the pool + Ok(conn) + } +} \ No newline at end of file diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index 5f883ed14..be966dc75 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,12 +1,15 @@ use crate::aws::kms; +use crate::db::pgschema_manager::{ConnectionConfig, PgSchemaManager}; use crate::helpers::get_from_env_unsafe; +use crate::service::types::AppEnv; use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; +use std::collections::HashSet; use urlencoding::encode; -async fn get_database_url() -> String { +pub async fn get_database_url() -> String { let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); let kms_client = kms::new_client(); let db_password_raw = kms::decrypt(kms_client, "DB_PASSWORD").await; @@ -25,3 +28,38 @@ pub async fn get_pool() -> Pool> { .build(manager) .expect("Error building a connection pool") } + +pub async fn init_pool_manager( + tenants: HashSet, + enable_tenant_and_scope: bool, + app_env: AppEnv, + max_pool_size: u32, +) -> PgSchemaManager { + let database_url = get_database_url().await; + let namespaces = match (enable_tenant_and_scope, app_env) { + (true, _) => tenants + .iter() + .flat_map(|tenant| { + [ + format!("{}_cac", tenant), + format!("{}_experimentation", tenant), + ] + }) + .collect::>(), + (false, _) => vec!["cac_v1".to_string()], + }; + + let connection_configs = namespaces + .iter() + .map(|namespace| { + ConnectionConfig::new( + namespace.to_string(), + database_url.clone(), + namespace.to_string(), + max_pool_size, + ) + }) + .collect::>(); + + PgSchemaManager::from(connection_configs) +} \ No newline at end of file diff --git a/crates/service-utils/src/middlewares/app_scope.rs b/crates/service-utils/src/middlewares/app_scope.rs index 60e528abc..0dd8778a1 100644 --- a/crates/service-utils/src/middlewares/app_scope.rs +++ b/crates/service-utils/src/middlewares/app_scope.rs @@ -7,19 +7,21 @@ use actix_web::{ }; use futures_util::future::LocalBoxFuture; -pub struct AppExecutionScope { +use std::rc::Rc; + +pub struct AppExecutionScopeMiddlewareFactory { scope: AppScope, } -impl AppExecutionScope { +impl AppExecutionScopeMiddlewareFactory { pub fn new(scope: AppScope) -> Self { - AppExecutionScope { scope } + AppExecutionScopeMiddlewareFactory { scope: scope } } } -impl Transform for AppExecutionScope +impl Transform for AppExecutionScopeMiddlewareFactory where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, B: 'static, { @@ -31,20 +33,20 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(AppExecutionScopeMiddleware { - service, - scope: self.scope, + service: Rc::new(service), + scope: self.scope.clone(), })) } } pub struct AppExecutionScopeMiddleware { - service: S, + service: Rc, scope: AppScope, } impl Service for AppExecutionScopeMiddleware where - S: Service, Error = Error>, + S: Service, Error = Error> + 'static, S::Future: 'static, B: 'static, { @@ -55,12 +57,14 @@ where forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { - req.extensions_mut().insert(self.scope); - let future = self.service.call(req); + let srv = self.service.clone(); + let scope = self.scope; Box::pin(async move { - let res = future.await?; + req.extensions_mut().insert(scope); + let res = srv.call(req).await?; + Ok(res) }) } -} +} \ No newline at end of file diff --git a/crates/service-utils/src/middlewares/mod.rs b/crates/service-utils/src/middlewares/mod.rs index 4f605bffa..d842a10db 100644 --- a/crates/service-utils/src/middlewares/mod.rs +++ b/crates/service-utils/src/middlewares/mod.rs @@ -1 +1,2 @@ pub mod app_scope; +pub mod tenant; \ No newline at end of file diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs new file mode 100644 index 000000000..f910274cf --- /dev/null +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -0,0 +1,84 @@ +use std::future::{ready, Ready}; + +use crate::service::types::{AppState, Tenant}; +use actix_web::{ + web::Data, error, + http::header::HeaderValue, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + Error, HttpMessage, +}; +use futures_util::future::LocalBoxFuture; +use std::rc::Rc; + +pub struct TenantMiddlewareFactory; +impl Transform for TenantMiddlewareFactory +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = TenantMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(TenantMiddleware { service: Rc::new(service) })) + } +} + +pub struct TenantMiddleware { + service: Rc, +} + +impl Service for TenantMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); + + Box::pin(async move { + let app_state = match req.app_data::>() { + Some(val) => val, + None => { + log::error!("app state not set"); + return Err(error::ErrorInternalServerError("")) + } + }; + + if app_state.enable_tenant_and_scope { + let tenant = req + .headers() + .get("x-tenant") + .map_or(None, |header_value: &HeaderValue| header_value.to_str().ok()) + .map(|header_str| header_str.to_string()); + + let validated_tenant: Tenant = match tenant { + Some(val) if app_state.tenants.contains(&val) => Tenant(val), + Some(_) => { + return Err(error::ErrorBadRequest("invalid x-tenant value")); + }, + None => { + return Err(error::ErrorBadRequest("x-tenant not set")); + } + }; + + req.extensions_mut().insert(validated_tenant); + } + + let res = srv.call(req).await?; + + Ok(res) + }) + } +} \ No newline at end of file diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index cf27db2cf..490b73a2c 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -1,33 +1,38 @@ -use diesel::{ - r2d2::{ConnectionManager, Pool, PooledConnection}, - PgConnection, -}; +use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager}; +use derive_more::{Deref, DerefMut}; use jsonschema::JSONSchema; -use std::{collections::HashSet, future::{ready, Ready}}; - -use actix_web::{ - error, web::Data, Error, FromRequest, HttpMessage, +use std::{ + collections::HashSet, + future::{ready, Ready}, + str::FromStr, }; +use actix_web::{error, web::Data, Error, FromRequest, HttpMessage}; + use snowflake::SnowflakeIdGenerator; use std::sync::Mutex; -use dashboard_auth::types::Tenant; - pub struct ExperimentationFlags { pub allow_same_keys_overlapping_ctx: bool, pub allow_diff_keys_overlapping_ctx: bool, pub allow_same_keys_non_overlapping_ctx: bool, } +#[derive(Copy, Clone, Debug)] +pub enum AppEnv { + PROD, + SANDBOX, + DEV, +} + pub struct AppState { pub cac_host: String, - pub prod: bool, + pub app_env: AppEnv, pub tenants: HashSet, pub cac_version: String, pub admin_token: String, - pub db_pool: Pool>, + pub db_pool: PgSchemaManager, pub default_config_validation_schema: JSONSchema, pub meta_schema: JSONSchema, pub experimentation_flags: ExperimentationFlags, @@ -35,25 +40,102 @@ pub struct AppState { pub enable_tenant_and_scope: bool, } -#[derive(Copy, Clone, Debug)] +impl FromStr for AppEnv { + type Err = String; + fn from_str(val: &str) -> Result { + match val { + "PROD" => Ok(AppEnv::PROD), + "SANDBOX" => Ok(AppEnv::SANDBOX), + "DEV" => Ok(AppEnv::DEV), + _ => Err("invalid app env!!".to_string()), + } + } +} + +#[derive(Copy, Clone, Debug, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] pub enum AppScope { CAC, EXPERIMENTATION, } +impl FromRequest for AppScope { + type Error = Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload + ) -> Self::Future { + let scope = req.extensions().get::().cloned(); + let result = match scope { + Some(v) => Ok(v), + None => Err(error::ErrorInternalServerError("app scope not set")), + }; + ready(result) + } +} -impl ToString for AppScope { - fn to_string(&self) -> String { - match self { - AppScope::CAC => String::from("cac"), - AppScope::EXPERIMENTATION => String::from("experimentation"), +#[derive(Deref, DerefMut, Clone, Debug)] +pub struct AppExecutionNamespace(pub String); +impl AppExecutionNamespace { + pub fn from_request_sync(req: &actix_web::HttpRequest) -> Result { + let app_state = match req.app_data::>() { + Some(val) => val, + None => { + log::error!("get_app_execution_namespace: AppState not set"); + return Err(error::ErrorInternalServerError("")); + } + }; + + let tenant = req.extensions().get::().cloned(); + let scope = req.extensions().get::().cloned(); + + match ( + app_state.enable_tenant_and_scope, + app_state.app_env, + tenant, + scope, + ) { + (false, _, _, _) => { + Ok(AppExecutionNamespace("cac_v1".to_string())) + }, + (true, _, Some(t), Some(s)) => Ok(AppExecutionNamespace(format!( + "{}_{}", + t.as_str(), + s.to_string() + ))), + (true, _, None, _) => { + log::error!( + "get_app_execution_namespace: Tenant not set in request extensions" + ); + Err(error::ErrorInternalServerError("")) + } + (true, _, _, None) => { + log::error!( + "get_app_execution_namespace: AppScope not set in request extensions" + ); + Err(error::ErrorInternalServerError("")) + } } } } -pub type DBConnection = PooledConnection>; +impl FromRequest for AppExecutionNamespace { + type Error = Error; + type Future = Ready>; -pub struct DbConnection(pub PooledConnection>); -impl FromRequest for DbConnection { + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + ready(AppExecutionNamespace::from_request_sync(req)) + } +} + + +#[derive(Deref, DerefMut, Clone, Debug)] +pub struct Tenant(pub String); +impl FromRequest for Tenant { type Error = Error; type Future = Ready>; @@ -61,71 +143,46 @@ impl FromRequest for DbConnection { req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload, ) -> Self::Future { - let app_state = match req.app_data::>() { - Some(state) => state, - None => { - log::info!("Unable to get app_data from request"); - return ready(Err(error::ErrorInternalServerError(""))); - } + let tenant = req.extensions().get::().cloned(); + let result = match tenant { + Some(v) => Ok(v), + None => Err(error::ErrorInternalServerError("tenant not set")), }; - let result = match app_state.db_pool.get() { - Ok(conn) => Ok(DbConnection(conn)), - Err(e) => { - log::info!("Unable to get db connection from pool, error: {e}"); - Err(error::ErrorInternalServerError("")) - } - }; - ready(result) } } -pub struct TenantNScope(pub String); -impl FromRequest for TenantNScope { +#[derive(Deref, DerefMut)] +pub struct DbConnection(pub PgSchemaConnection); +impl FromRequest for DbConnection { type Error = Error; - type Future = Ready>; + type Future = Ready>; fn from_request( req: &actix_web::HttpRequest, - payload: &mut actix_web::dev::Payload, + _: &mut actix_web::dev::Payload, ) -> Self::Future { + let namespace = match AppExecutionNamespace::from_request_sync(req) { + Ok(val) => val.as_str().to_string(), + Err(e) => { return ready(Err(e)); } + }; + let app_state = match req.app_data::>() { Some(state) => state, None => { - log::info!("Unable to get app_date from request"); + log::info!("DbConnection-FromRequest: Unable to get app_data from request"); return ready(Err(error::ErrorInternalServerError(""))); } }; - if !app_state.enable_tenant_and_scope { - let default_tenant = match app_state.prod { - true => "cac_v1".to_string(), - false => "public".to_string() - }; - - return ready(Ok(TenantNScope(default_tenant))); - } - - // extract headers for tenant id - let tenant = Tenant::from_request(req, payload) - .into_inner() - .map_or(None, |value: Tenant| Some(value.as_str().to_string())); - - let scope = req - .extensions() - .get::() - .and_then(|app_scope| Some(app_scope.to_string())); - - let tenant_n_scope:Result = match (tenant, scope) { - (Some(t), Some(s)) if app_state.tenants.contains(&t) => Ok(TenantNScope(format!("{t}_{s}"))), - (Some(_), Some(_)) => Err(error::ErrorBadRequest("invalid x-tenant value")), - (None, _) => Err(error::ErrorBadRequest("x-tenant not set")), - (_, None) => { - log::error!("AppScope not set for the request!"); - Err(error::ErrorInternalServerError("something went wrong")) + let result = match app_state.db_pool.get_conn(namespace) { + Ok(conn) => Ok(DbConnection(conn)), + Err(e) => { + log::info!("Unable to get db connection from pool, error: {e}"); + Err(error::ErrorInternalServerError("")) } }; - ready(tenant_n_scope) + ready(result) } } \ No newline at end of file diff --git a/makefile b/makefile index 4d384df42..7887213c9 100644 --- a/makefile +++ b/makefile @@ -50,6 +50,9 @@ migration: make cac-migration make exp-migration +legacy_db_setup: + grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/legacy-db-setup.sh + tenant: grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/create-tenant.sh $(TENANT) @@ -75,6 +78,7 @@ setup: # to prevent update of schema.rs for both cac and experimentation. # NOTE: The container spinned-up here is the actual container being used in development make db-init + make legacy_db_setup kill: -pkill -f target/debug/context-aware-config & @@ -91,12 +95,15 @@ run: sleep 0.5; \ done # make setup + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") npm ci --loglevel=error + cargo test make setup make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ diff --git a/scripts/cac-init.sql b/scripts/cac-init.sql deleted file mode 100644 index 1f4198699..000000000 --- a/scripts/cac-init.sql +++ /dev/null @@ -1,221 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- {{schema}}.event_log definition - --- Drop table - --- DROP TABLE {{schema}}.event_log; - -CREATE TABLE {{schema}}.event_log ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - table_name text NOT NULL, - user_name text NOT NULL, - "timestamp" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - "action" text NOT NULL, - original_data json NULL, - new_data json NULL, - query text NOT NULL, - CONSTRAINT event_log_pkey PRIMARY KEY (id, "timestamp") -) -PARTITION BY RANGE ("timestamp"); -CREATE INDEX event_log_action_index ON ONLY {{schema}}.event_log USING btree (action) INCLUDE ("timestamp", table_name); -CREATE INDEX event_log_table_name_index ON ONLY {{schema}}.event_log USING btree (table_name) INCLUDE (action, "timestamp"); -CREATE INDEX event_log_timestamp_index ON ONLY {{schema}}.event_log USING btree ("timestamp") INCLUDE (action, table_name); - - --- {{schema}}.event_log_y2023m08 definition - -CREATE TABLE {{schema}}.event_log_y2023m08 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-08-01 00:00:00') TO ('2023-09-01 00:00:00'); - - --- {{schema}}.event_log_y2023m09 definition - -CREATE TABLE {{schema}}.event_log_y2023m09 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-09-01 00:00:00') TO ('2023-10-01 00:00:00'); - - --- {{schema}}.event_log_y2023m10 definition - -CREATE TABLE {{schema}}.event_log_y2023m10 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-10-01 00:00:00') TO ('2023-11-01 00:00:00'); - - --- {{schema}}.event_log_y2023m11 definition - -CREATE TABLE {{schema}}.event_log_y2023m11 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-11-01 00:00:00') TO ('2023-12-01 00:00:00'); - - --- {{schema}}.event_log_y2023m12 definition - -CREATE TABLE {{schema}}.event_log_y2023m12 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-12-01 00:00:00') TO ('2024-01-01 00:00:00'); - - --- {{schema}}.event_log_y2024m01 definition - -CREATE TABLE {{schema}}.event_log_y2024m01 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-01-01 00:00:00') TO ('2024-02-01 00:00:00'); - - --- {{schema}}.event_log_y2024m02 definition - -CREATE TABLE {{schema}}.event_log_y2024m02 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-02-01 00:00:00') TO ('2024-03-01 00:00:00'); - - --- {{schema}}.event_log_y2024m03 definition - -CREATE TABLE {{schema}}.event_log_y2024m03 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-03-01 00:00:00') TO ('2024-04-01 00:00:00'); - - --- {{schema}}.event_log_y2024m04 definition - -CREATE TABLE {{schema}}.event_log_y2024m04 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-04-01 00:00:00') TO ('2024-05-01 00:00:00'); - - --- {{schema}}.event_log_y2024m05 definition - -CREATE TABLE {{schema}}.event_log_y2024m05 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-05-01 00:00:00') TO ('2024-06-01 00:00:00'); - - --- {{schema}}.event_log_y2024m06 definition - -CREATE TABLE {{schema}}.event_log_y2024m06 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-06-01 00:00:00') TO ('2024-07-01 00:00:00'); - - --- {{schema}}.event_log_y2024m07 definition - -CREATE TABLE {{schema}}.event_log_y2024m07 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-07-01 00:00:00') TO ('2024-08-01 00:00:00'); - - -CREATE OR REPLACE FUNCTION {{schema}}.event_logger() - RETURNS trigger - LANGUAGE plpgsql -AS $function$ -DECLARE - old_data json; - new_data json; -BEGIN - IF (TG_OP = 'UPDATE') THEN - old_data := row_to_json(OLD); - new_data := row_to_json(NEW); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, original_data, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - new_data, - current_query() - ); - - ELSIF (TG_OP = 'DELETE') THEN - old_data := row_to_json(OLD); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, original_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - current_query() - ); - - ELSIF (TG_OP = 'INSERT') THEN - new_data = row_to_json(NEW); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - new_data, - current_query() - ); - END IF; - - RETURN NULL; -END; -$function$ -; - --- {{schema}}.contexts definition - --- Drop table - --- DROP TABLE {{schema}}.contexts; - -CREATE TABLE {{schema}}.contexts ( - id varchar NOT NULL, - value json NOT NULL, - override_id varchar NOT NULL, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by varchar NOT NULL, - priority int4 NOT NULL DEFAULT 1, - override json NOT NULL DEFAULT '{}'::json, - CONSTRAINT contexts_pkey PRIMARY KEY (id) -); - --- Table Triggers - -create trigger contexts_audit after -insert - or -delete - or -update - on - {{schema}}.contexts for each row execute function {{schema}}.event_logger(); - - --- {{schema}}.default_configs definition - --- Drop table - --- DROP TABLE {{schema}}.default_configs; - -CREATE TABLE {{schema}}.default_configs ( - "key" varchar NOT NULL, - value json NOT NULL, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by varchar NOT NULL, - "schema" json NOT NULL DEFAULT '{}'::json, - CONSTRAINT default_configs_pkey PRIMARY KEY (key) -); - --- Table Triggers - -create trigger default_configs_audit after -insert - or -delete - or -update - on - {{schema}}.default_configs for each row execute function {{schema}}.event_logger(); - - --- {{schema}}.dimensions definition - --- Drop table - --- DROP TABLE {{schema}}.dimensions; - -CREATE TABLE {{schema}}.dimensions ( - dimension varchar NOT NULL, - priority int4 NOT NULL, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by varchar NOT NULL, - "schema" json NOT NULL DEFAULT '{}'::json, - CONSTRAINT dimensions_pkey PRIMARY KEY (dimension) -); - --- Table Triggers - -create trigger dimensions_audit after -insert - or -delete - or -update - on - {{schema}}.dimensions for each row execute function {{schema}}.event_logger(); \ No newline at end of file diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index c638f50bb..a9f938453 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -1,11 +1,4 @@ #!/bin/bash - -HOST="localhost" -PORT="5432" -USER="postgres" -PASSWORD="docker" -DATABASE="config" - TENANT=$1 DB_URL=$2 @@ -13,17 +6,19 @@ echo "Tenant ID ==> $TENANT" echo "DB URL ==> $DB_URL" # Creating schemas -SCHEMA1="${TENANT}_cac" -SCHEMA2="${TENANT}_experimentation" +CAC_SCHEMA="${TENANT}_cac" +EXP_SCHEMA="${TENANT}_experimentation" + +echo "Creating Schemas ==> $CAC_SCHEMA, $EXP_SCHEMA" -echo "Creating Schemas ==> $SCHEMA1, $SCHEMA2" +cp -r "crates/context-aware-config/migrations/." "crates/context-aware-config/${TENANT}_migrations" +find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${CAC_SCHEMA}/g" -psql "$DB_URL" -c "CREATE SCHEMA ${SCHEMA1}" -psql "$DB_URL" -c "CREATE SCHEMA ${SCHEMA2}" +cp -r "crates/experimentation-platform/migrations/." "crates/experimentation-platform/${TENANT}_migrations" +find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${EXP_SCHEMA}/g" -# Running migrations in created schemas -sed "s/{{schema}}/${SCHEMA1}/g" "$PWD/scripts/cac-init.sql" > $PWD/scripts/cac-init-with-schema.sql -sed "s/{{schema}}/${SCHEMA2}/g" "$PWD/scripts/experimentation-init.sql" > $PWD/scripts/experimentation-init-with-schema.sql +find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" | xargs psql "$DB_URL" -f +find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" | xargs psql "$DB_URL" -f -psql "$DB_URL" -f "$PWD/scripts/cac-init-with-schema.sql" -psql "$DB_URL" -f "$PWD/scripts/experimentation-init-with-schema.sql" \ No newline at end of file +rm -rf "crates/context-aware-config/${TENANT}_migrations" +rm -rf "crates/experimentation-platform/${TENANT}_migrations" \ No newline at end of file diff --git a/scripts/experimentation-init.sql b/scripts/experimentation-init.sql deleted file mode 100644 index fd7d4b4b1..000000000 --- a/scripts/experimentation-init.sql +++ /dev/null @@ -1,187 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- DROP TYPE {{schema}}.experiment_status_type; - -CREATE TYPE {{schema}}.experiment_status_type AS ENUM ( - 'CREATED', - 'CONCLUDED', - 'INPROGRESS'); - --- DROP DOMAIN {{schema}}.not_null_text; - -CREATE DOMAIN {{schema}}.not_null_text AS text not null; - - --- {{schema}}.event_log definition - --- Drop table - --- DROP TABLE {{schema}}.event_log; - -CREATE TABLE {{schema}}.event_log ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - table_name text NOT NULL, - user_name text NOT NULL, - "timestamp" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - "action" text NOT NULL, - original_data json NULL, - new_data json NULL, - query text NOT NULL, - CONSTRAINT event_log_pkey PRIMARY KEY (id, "timestamp") -) -PARTITION BY RANGE ("timestamp"); -CREATE INDEX event_log_action_index ON ONLY {{schema}}.event_log USING btree (action) INCLUDE ("timestamp", table_name); -CREATE INDEX event_log_table_name_index ON ONLY {{schema}}.event_log USING btree (table_name) INCLUDE (action, "timestamp"); -CREATE INDEX event_log_timestamp_index ON ONLY {{schema}}.event_log USING btree ("timestamp") INCLUDE (action, table_name); - -CREATE OR REPLACE FUNCTION {{schema}}.event_logger() - RETURNS trigger - LANGUAGE plpgsql -AS $function$ -DECLARE - old_data json; - new_data json; -BEGIN - IF (TG_OP = 'UPDATE') THEN - old_data := row_to_json(OLD); - new_data := row_to_json(NEW); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, original_data, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - new_data, - current_query() - ); - - ELSIF (TG_OP = 'DELETE') THEN - old_data := row_to_json(OLD); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, original_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - current_query() - ); - - ELSIF (TG_OP = 'INSERT') THEN - new_data = row_to_json(NEW); - - INSERT INTO {{schema}}.event_log - (table_name, user_name, action, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - new_data, - current_query() - ); - END IF; - - RETURN NULL; -END; -$function$ -; - --- {{schema}}.experiments definition - --- Drop table - --- DROP TABLE {{schema}}.experiments; - -CREATE TABLE {{schema}}.experiments ( - id int8 NOT NULL, - created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by text NOT NULL, - last_modified timestamptz NOT NULL DEFAULT now(), - "name" text NOT NULL, - override_keys {{schema}}.not_null_text [] NOT NULL, - status {{schema}}.experiment_status_type NOT NULL, - traffic_percentage int4 NOT NULL, - context json NOT NULL, - variants json NOT NULL, - last_modified_by text NOT NULL DEFAULT 'Null'::text, - chosen_variant text NULL, - CONSTRAINT experiments_pkey PRIMARY KEY (id), - CONSTRAINT experiments_traffic_percentage_check CHECK ((traffic_percentage >= 0)) -); -CREATE INDEX experiment_created_date_index ON {{schema}}.experiments USING btree (created_at) INCLUDE (id); -CREATE INDEX experiment_last_modified_index ON {{schema}}.experiments USING btree (last_modified) INCLUDE (id, created_at); -CREATE INDEX experiment_status_index ON {{schema}}.experiments USING btree (status) INCLUDE (created_at, last_modified); - --- Table Triggers - -create trigger experiments_audit after -insert - or -delete - or -update - on - {{schema}}.experiments for each row execute function {{schema}}.event_logger(); - - --- {{schema}}.event_log_y2023m08 definition - -CREATE TABLE {{schema}}.event_log_y2023m08 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-08-01 00:00:00') TO ('2023-09-01 00:00:00'); - - --- {{schema}}.event_log_y2023m09 definition - -CREATE TABLE {{schema}}.event_log_y2023m09 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-09-01 00:00:00') TO ('2023-10-01 00:00:00'); - - --- {{schema}}.event_log_y2023m10 definition - -CREATE TABLE {{schema}}.event_log_y2023m10 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-10-01 00:00:00') TO ('2023-11-01 00:00:00'); - - --- {{schema}}.event_log_y2023m11 definition - -CREATE TABLE {{schema}}.event_log_y2023m11 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-11-01 00:00:00') TO ('2023-12-01 00:00:00'); - - --- {{schema}}.event_log_y2023m12 definition - -CREATE TABLE {{schema}}.event_log_y2023m12 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2023-12-01 00:00:00') TO ('2024-01-01 00:00:00'); - - --- {{schema}}.event_log_y2024m01 definition - -CREATE TABLE {{schema}}.event_log_y2024m01 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-01-01 00:00:00') TO ('2024-02-01 00:00:00'); - - --- {{schema}}.event_log_y2024m02 definition - -CREATE TABLE {{schema}}.event_log_y2024m02 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-02-01 00:00:00') TO ('2024-03-01 00:00:00'); - - --- {{schema}}.event_log_y2024m03 definition - -CREATE TABLE {{schema}}.event_log_y2024m03 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-03-01 00:00:00') TO ('2024-04-01 00:00:00'); - - --- {{schema}}.event_log_y2024m04 definition - -CREATE TABLE {{schema}}.event_log_y2024m04 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-04-01 00:00:00') TO ('2024-05-01 00:00:00'); - - --- {{schema}}.event_log_y2024m05 definition - -CREATE TABLE {{schema}}.event_log_y2024m05 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-05-01 00:00:00') TO ('2024-06-01 00:00:00'); - - --- {{schema}}.event_log_y2024m06 definition - -CREATE TABLE {{schema}}.event_log_y2024m06 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-06-01 00:00:00') TO ('2024-07-01 00:00:00'); - - --- {{schema}}.event_log_y2024m07 definition - -CREATE TABLE {{schema}}.event_log_y2024m07 PARTITION OF {{schema}}.event_log FOR VALUES FROM ('2024-07-01 00:00:00') TO ('2024-08-01 00:00:00'); \ No newline at end of file diff --git a/scripts/legacy-db-setup.sh b/scripts/legacy-db-setup.sh new file mode 100755 index 000000000..c7884c248 --- /dev/null +++ b/scripts/legacy-db-setup.sh @@ -0,0 +1,17 @@ +DB_URL=$1 + +cp -r "crates/context-aware-config/migrations/." "crates/context-aware-config/cac_v1_migrations" +find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; + +find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec cat {} \; + +xargs cp -r "crates/experimentation-platform/migrations/." "crates/experimentation-platform/cac_v1_migrations" +find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; + +find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec cat {} \; + +find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; +find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; + +rm -rf "crates/context-aware-config/cac_v1_migrations" +rm -rf "crates/experimentation-platform/cac_v1_migrations" \ No newline at end of file From 7c429a7ba2415e79fb0167d74634f2dbc9dc6f9e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 25 Oct 2023 11:18:55 +0000 Subject: [PATCH 161/352] chore(version): v0.11.0 [skip ci] --- CHANGELOG.md | 14 +++ Cargo.lock | 98 +++++++++++--------- crates/context-aware-config/CHANGELOG.md | 9 ++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 9 ++ crates/experimentation-platform/Cargo.toml | 4 +- crates/service-utils/CHANGELOG.md | 7 ++ crates/service-utils/Cargo.toml | 4 +- 8 files changed, 98 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e893f790..90e003de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.11.0 - 2023-10-25 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.6.0 +- service-utils bumped to service-utils-v0.7.0 +- context-aware-config bumped to context-aware-config-v0.10.0 +### Global changes +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + ## v0.10.0 - 2023-10-20 ### Package updates - context-aware-config bumped to context-aware-config-v0.9.0 diff --git a/Cargo.lock b/Cargo.lock index d4aee0713..887b209fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,7 +701,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.9.0" +version = "0.10.0" dependencies = [ "actix", "actix-cors", @@ -727,7 +727,7 @@ dependencies = [ "jsonschema", "log", "rand 0.8.5", - "reqwest 0.11.20", + "reqwest 0.9.24", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -912,6 +912,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1173,7 +1182,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix", "actix-web", @@ -1667,16 +1676,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" dependencies = [ - "futures-util", - "http 0.2.9", - "hyper 0.14.26", + "bytes 0.4.12", + "ct-logs", + "futures 0.1.31", + "hyper 0.12.36", "rustls", - "tokio 1.29.1", + "tokio-io", "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -2726,11 +2738,13 @@ dependencies = [ "futures 0.1.31", "http 0.1.21", "hyper 0.12.36", + "hyper-rustls", "hyper-tls 0.3.2", "log", "mime", "mime_guess", "native-tls", + "rustls", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2738,10 +2752,12 @@ dependencies = [ "tokio 0.1.22", "tokio-executor", "tokio-io", + "tokio-rustls", "tokio-threadpool", "tokio-timer", "url 1.7.2", "uuid 0.7.4", + "webpki-roots", "winreg 0.6.2", ] @@ -2760,7 +2776,6 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper 0.14.26", - "hyper-rustls", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2770,20 +2785,16 @@ dependencies = [ "once_cell", "percent-encoding 2.2.0", "pin-project-lite", - "rustls", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded 0.7.1", "tokio 1.29.1", "tokio-native-tls", - "tokio-rustls", "tower-service", "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg 0.50.0", ] @@ -2931,33 +2942,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ + "base64 0.10.1", "log", "ring", - "rustls-webpki", "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" -dependencies = [ - "ring", - "untrusted", + "webpki", ] [[package]] @@ -3004,9 +2997,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -3122,7 +3115,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.6.0" +version = "0.7.0" dependencies = [ "actix", "actix-web", @@ -3620,12 +3613,16 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", "rustls", - "tokio 1.29.1", + "tokio-io", + "webpki", ] [[package]] @@ -4097,11 +4094,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" +dependencies = [ + "webpki", +] [[package]] name = "winapi" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 8ca84b5a1..d336e7ba1 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.10.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.9.0 - 2023-10-20 #### Features - PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 7b0e509fb..10ecc750d 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.9.0" +version = "0.10.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index adef729f3..00a0208c4 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.6.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.5.0 - 2023-10-10 #### Bug Fixes - validating override_keys for unique entries - (36cf523) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 9fd71cbf4..f0c0a2331 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.5.0" +version = "0.6.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -32,4 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 359c31ccd..fb7ef89a9 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.7.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.6.0 - 2023-10-20 #### Features - PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 9edbac676..dc4c3c302 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.6.0" +version = "0.7.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -31,4 +31,4 @@ serde_json = {version = "1.0"} strum_macros = "^0.24" strum = {version = "^0.24"} derive_more = "^0.99" -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} From ae8c9be6bf688470373e37c2bce3b3e7daa4615c Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Thu, 26 Oct 2023 13:22:59 +0530 Subject: [PATCH 162/352] fixed priority in the move api --- .../src/api/context/handlers.rs | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 90cb8a002..2cd34e4db 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use crate::{ api::{ context::types::{ - ContextAction, ContextBulkResponse, DimensionCondition, PaginationParams, - PutReq, PutResp, MoveReq + ContextAction, ContextBulkResponse, DimensionCondition, MoveReq, + PaginationParams, PutReq, PutResp, }, dimension::get_all_dimension_schema_map, }, @@ -32,9 +32,9 @@ use diesel::{ result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; +use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{from_value, json, Map, Value, Value::Null}; use service_utils::{helpers::ToActixErr, service::types::DbConnection}; -use jsonschema::{Draft, JSONSchema, ValidationError}; pub fn endpoints() -> Scope { Scope::new("") @@ -66,10 +66,7 @@ fn validate_dimensions_and_calculate_priority( )) .copied() } else { - validate_dimensions_and_calculate_priority( - val, - dimension_schema_map, - ) + validate_dimensions_and_calculate_priority(val, dimension_schema_map) } }; @@ -89,7 +86,9 @@ fn validate_dimensions_and_calculate_priority( val = Some(i.clone()); } - if let (Some(_dimension_value), Some(_dimension_condition)) = (&val, &condition) { + if let (Some(_dimension_value), Some(_dimension_condition)) = + (&val, &condition) + { break; } } @@ -109,11 +108,8 @@ fn validate_dimensions_and_calculate_priority( }; arr.iter().try_fold(0, |acc, item| { - validate_dimensions_and_calculate_priority( - item, - dimension_schema_map, - ) - .map(|res| res + acc) + validate_dimensions_and_calculate_priority(item, dimension_schema_map) + .map(|res| res + acc) }) } _ => Ok(0), @@ -245,8 +241,7 @@ fn put( let new_ctx = create_ctx_from_put_req(req, conn, user)?; if already_under_txn { - diesel::sql_query("SAVEPOINT put_ctx_savepoint") - .execute(conn)?; + diesel::sql_query("SAVEPOINT put_ctx_savepoint").execute(conn)?; } let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); @@ -254,8 +249,7 @@ fn put( Ok(_) => Ok(get_put_resp(new_ctx)), Err(DatabaseError(UniqueViolation, _)) => { if already_under_txn { - diesel::sql_query("ROLLBACK TO put_ctx_savepoint") - .execute(conn)?; + diesel::sql_query("ROLLBACK TO put_ctx_savepoint").execute(conn)?; } update_override_of_existing_ctx(conn, new_ctx) } @@ -292,7 +286,10 @@ fn r#move( let ctx_condition = Value::Object(req.context); let new_ctx_id = generate_context_id(&ctx_condition); let dimension_schema_map = get_all_dimension_schema_map(conn)?; - let priority = match validate_dimensions_and_calculate_priority(&ctx_condition, &dimension_schema_map) { + let priority = match validate_dimensions_and_calculate_priority( + &ctx_condition, + &dimension_schema_map, + ) { Ok(0) => { return Err(anyhow!(String::from("No dimension found in context"))); } @@ -303,13 +300,16 @@ fn r#move( }; if already_under_txn { - diesel::sql_query("SAVEPOINT update_ctx_savepoint") - .execute(conn)?; + diesel::sql_query("SAVEPOINT update_ctx_savepoint").execute(conn)?; } let context = diesel::update(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) - .set((dsl::id.eq(&new_ctx_id), dsl::value.eq(&ctx_condition))) + .set(( + dsl::id.eq(&new_ctx_id), + dsl::value.eq(&ctx_condition), + dsl::priority.eq(priority), + )) .get_result(conn); let contruct_new_ctx_with_old_overrides = |ctx: Context| Context { @@ -346,8 +346,7 @@ fn r#move( Ok(ctx) => Ok(get_put_resp(ctx)), Err(DatabaseError(UniqueViolation, _)) => { if already_under_txn { - diesel::sql_query("ROLLBACK TO update_ctx_savepoint") - .execute(conn)?; + diesel::sql_query("ROLLBACK TO update_ctx_savepoint").execute(conn)?; } handle_unique_violation(conn, already_under_txn) } @@ -531,4 +530,4 @@ async fn bulk_operations( Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} \ No newline at end of file +} From f524388528fcca3b7c39b38b96c16d7462972b3d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Thu, 19 Oct 2023 17:51:53 +0530 Subject: [PATCH 163/352] feat: multi-tenant support for client libraries --- Cargo.lock | 9 ++- crates/cac_client/Cargo.toml | 2 + crates/cac_client/src/lib.rs | 76 +++++++++++++++++-- crates/superposition_client/Cargo.toml | 3 + crates/superposition_client/src/lib.rs | 47 +++++++++++- crates/superposition_client/src/types.rs | 3 +- .../src/main.rs | 5 +- 7 files changed, 132 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 887b209fc..d35effe57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,9 +581,11 @@ version = "0.2.1" dependencies = [ "actix-web", "chrono", + "derive_more", "json-patch", "jsonlogic", "log", + "once_cell", "reqwest 0.11.20", "serde", "serde_json", @@ -2292,9 +2294,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -3274,8 +3276,11 @@ name = "superposition_client" version = "0.2.0" dependencies = [ "chrono", + "derive_more", "dotenv", "jsonlogic", + "log", + "once_cell", "reqwest 0.11.20", "serde", "serde_json", diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 0fe1de4f8..be68ed085 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -10,7 +10,9 @@ actix-web = "4.3.1" chrono = "0.4.26" jsonlogic = "0.5.1" reqwest = { version = "0.11.18", features = ["json"]} +once_cell = { version = "1.18.0" } serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" log = "^0.4" json-patch = "1.0.0" +derive_more = "^0.99" \ No newline at end of file diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 76f321650..13bb8e630 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -9,8 +9,9 @@ use chrono::{DateTime, Utc}; use reqwest::{RequestBuilder, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::{convert::identity, sync::RwLock, time::Duration}; +use std::{convert::identity, sync::{RwLock, Arc}, time::Duration, collections::HashMap}; use utils::core::MapError; +use derive_more::{Deref, DerefMut}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { @@ -27,6 +28,7 @@ pub struct Config { #[derive(Clone)] pub struct Client { + tenant: String, reqw: Data, polling_interval: Duration, last_modified: Data>>, @@ -40,18 +42,23 @@ fn clone_reqw(reqw: &RequestBuilder) -> Result { impl Client { pub async fn new( + tenant: String, update_config_periodically: bool, polling_interval: Duration, hostname: String, ) -> Result { let reqw_client = reqwest::Client::builder().build().map_err_to_string()?; let cac_endpoint = format!("{hostname}/config"); - let reqw = reqw_client.get(cac_endpoint); + let reqw = reqw_client + .get(cac_endpoint) + .header("x-tenant", tenant.to_string()); + let reqwc = clone_reqw(&reqw)?; let resp = reqwc.send().await.map_err_to_string()?; let config = resp.json::().await.map_err_to_string()?; let timestamp = Utc::now(); let client = Client { + tenant, reqw: Data::new(reqw), polling_interval, last_modified: Data::new(RwLock::new(timestamp)), @@ -69,10 +76,10 @@ impl Client { let resp = reqw.send().await.map_err_to_string()?; match resp.status() { StatusCode::NOT_MODIFIED => { - return Err(String::from("CAC: skipping update, remote not modified")); + return Err(String::from(format!("{} CAC: skipping update, remote not modified", self.tenant))); } - StatusCode::OK => log::info!("CAC: new config received, updating"), - x => return Err(format!("CAC: fetch failed, status: {}", x,)), + StatusCode::OK => log::info!("{}", format!("{} CAC: new config received, updating", self.tenant)), + x => return Err(format!("{} CAC: fetch failed, status: {}", self.tenant, x)), }; resp.json::().await.map_err_to_string() } @@ -83,7 +90,7 @@ impl Client { let mut last_modified = self.last_modified.write().map_err_to_string()?; *config = fetched_config; *last_modified = Utc::now(); - Ok("CAC updated successfully".to_string()) + Ok(format!("{}: CAC updated successfully", self.tenant)) } pub async fn start_polling_update(self) { @@ -119,4 +126,59 @@ impl Client { } } -pub use eval::eval_cac; +#[derive(Deref, DerefMut)] +pub struct ClientFactory ( RwLock>> ); +impl ClientFactory { + pub async fn create_client( + &self, + tenant: String, + update_config_periodically: bool, + polling_interval: Duration, + hostname: String, + ) -> Result, String> { + let mut factory = match self.write() { + Ok(factory) => factory, + Err(e) => { + log::error!("CAC_CLIENT_FACTORY: failed to acquire write lock {}", e); + return Err("CAC_CLIENT_FACTORY: Failed to create client".to_string()); + } + }; + + if let Some(client) = factory.get(&tenant) { + return Ok(client.clone()); + } + + let client = Arc::new( + Client::new( + tenant.to_string(), + update_config_periodically, + polling_interval, + hostname + ).await? + ); + factory.insert(tenant.to_string(), client.clone()); + return Ok(client.clone()); + } + + pub fn get_client(&self, tenant: String) -> Result, String> { + let factory = match self.read() { + Ok(factory) => factory, + Err(e) => { + log::error!("CAC_CLIENT_FACTORY: failed to acquire read lock {}", e); + return Err("CAC_CLIENT_FACTORY: Failed to acquire client.".to_string()); + } + }; + + match factory.get(&tenant) { + Some(client) => Ok(client.clone()), + None => Err("No such tenant found".to_string()) + } + } +} + +use once_cell::sync::Lazy; +pub static CLIENT_FACTORY: Lazy = Lazy::new(|| { + ClientFactory(RwLock::new(HashMap::new())) +}); + +pub use eval::eval_cac; \ No newline at end of file diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index b153a6fee..8fdbfddfa 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -7,7 +7,10 @@ edition = "2021" chrono = "0.4.26" jsonlogic = "0.5.1" reqwest = { version = "0.11.18", features = ["json"]} +once_cell = { version = "1.18.0" } serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" tokio = {version = "1.29.1", features = ["full"]} dotenv = "0.15.0" +derive_more = "^0.99" +log = "^0.4" \ No newline at end of file diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index ddfc9dd5b..039942b40 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -9,6 +9,7 @@ use tokio::{ }; pub use types::{Config, Experiment, Experiments, Variants}; use types::{ExperimentStore, ListExperimentsResponse, Variant, VariantType}; +use derive_more::{Deref, DerefMut}; #[derive(Clone, Debug)] pub struct Client { @@ -33,7 +34,7 @@ impl Client { } } - pub async fn run_polling_updates(self) { + pub async fn run_polling_updates(self: Arc) { let poll_interval = self.client_config.poll_frequency; let hostname = &self.client_config.hostname; let mut interval = time::interval(Duration::from_secs(poll_interval)); @@ -47,6 +48,7 @@ impl Client { hostname.clone(), self.http_client.clone(), start_date.to_string(), + self.client_config.tenant.to_string(), ) .await .unwrap(); @@ -127,6 +129,7 @@ async fn get_experiments( hostname: String, http_client: reqwest::Client, start_date: String, + tenant: String, ) -> Result { let mut curr_exp_store: ExperimentStore = HashMap::new(); let requesting_count = 10; @@ -138,6 +141,7 @@ async fn get_experiments( ); let list_experiments_response = http_client .get(format!("{endpoint}&status=CREATED,INPROGRESS,CONCLUDED")) + .header("x-tenant", tenant.to_string()) .send() .await .unwrap() @@ -160,3 +164,44 @@ async fn get_experiments( Ok(curr_exp_store) } + +#[derive(Deref, DerefMut)] +pub struct ClientFactory ( RwLock>> ); +impl ClientFactory { + pub async fn create_client( + &self, + tenant: String, + poll_frequency: u64, + hostname: String, + ) -> Result, String> { + let mut factory = self.write().await; + + if let Some(client) = factory.get(&tenant) { + return Ok(client.clone()); + } + + let client = Arc::new( + Client::new(Config { + tenant: tenant.to_string(), + hostname: hostname, + poll_frequency: poll_frequency + }) + ); + + factory.insert(tenant.to_string(), client.clone()); + return Ok(client.clone()); + } + + pub async fn get_client(&self, tenant: String) -> Result, String> { + let factory = self.read().await; + match factory.get(&tenant) { + Some(client) => Ok(client.clone()), + None => Err("No such tenant found".to_string()) + } + } +} + +use once_cell::sync::Lazy; +pub static CLIENT_FACTORY: Lazy = Lazy::new(|| { + ClientFactory(RwLock::new(HashMap::new())) +}); \ No newline at end of file diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index f3916380a..a0948d7af 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -5,6 +5,7 @@ use serde_json::Value; #[derive(Clone, Debug)] pub struct Config { + pub tenant: String, pub hostname: String, pub poll_frequency: u64, } @@ -50,4 +51,4 @@ pub(crate) struct ListExperimentsResponse { pub(crate) total_items: i64, pub(crate) total_pages: i64, pub(crate) data: Experiments, -} +} \ No newline at end of file diff --git a/crates/superposition_client_integration_example/src/main.rs b/crates/superposition_client_integration_example/src/main.rs index 230029d8d..8733efad1 100644 --- a/crates/superposition_client_integration_example/src/main.rs +++ b/crates/superposition_client_integration_example/src/main.rs @@ -10,10 +10,11 @@ use superposition_client as exp; #[actix_web::main] async fn main() -> std::io::Result<()> { let client_configuration = exp::Config { + tenant: "tenant".to_string(), hostname: "http://localhost:8080".to_string(), poll_frequency: 10, }; - let client = exp::Client::new(client_configuration); + let client = std::sync::Arc::new(exp::Client::new(client_configuration)); rt::spawn(client.clone().run_polling_updates()); HttpServer::new(move || { App::new() @@ -43,4 +44,4 @@ async fn get_variants( let variant = state.get_applicable_variant(&contexts, toss).await; println!("variant value: {:?}", variant); HttpResponse::Ok().body("check your console") -} +} \ No newline at end of file From 3a604b03db5af667d1775a759af4dcdae866e7c1 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Fri, 27 Oct 2023 20:50:16 +0530 Subject: [PATCH 164/352] fix: fixed failing health check (x-tenant header not set) --- .env.example | 3 ++- crates/context-aware-config/src/main.rs | 6 ++++++ crates/service-utils/src/middlewares/tenant.rs | 5 ++++- crates/service-utils/src/service/types.rs | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 973544510..c2a962eb5 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,5 @@ DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 ENABLE_TENANT_AND_SCOPE=false -TENANTS=tenant1,tenant2,tenant3 \ No newline at end of file +TENANTS=mjos,sdk_config +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health" \ No newline at end of file diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 9e7276ed9..951a9667a 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -57,6 +57,11 @@ async fn main() -> Result<()> { .split(",") .map(|tenant| tenant.to_string()) .collect::>(); + let tenant_middleware_exclusion_list = get_from_env_unsafe::("TENANT_MIDDLEWARE_EXCLUSION_LIST") + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(",") + .map(String::from) + .collect::>(); let string_to_int = |s: &String| -> i32 { s.chars() @@ -117,6 +122,7 @@ async fn main() -> Result<()> { app_env: app_env.to_owned(), enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), tenants: tenants.to_owned(), + tenant_middleware_exclusion_list: tenant_middleware_exclusion_list.to_owned(), })) .wrap( actix_web::middleware::DefaultHeaders::new() diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index f910274cf..b9c1ceab9 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -56,7 +56,10 @@ where } }; - if app_state.enable_tenant_and_scope { + let request_path = req.uri().path(); + let is_excluded: bool = app_state.tenant_middleware_exclusion_list.contains(request_path); + + if !is_excluded && app_state.enable_tenant_and_scope { let tenant = req .headers() .get("x-tenant") diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 490b73a2c..da609f647 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -38,6 +38,7 @@ pub struct AppState { pub experimentation_flags: ExperimentationFlags, pub snowflake_generator: Mutex, pub enable_tenant_and_scope: bool, + pub tenant_middleware_exclusion_list: HashSet, } impl FromStr for AppEnv { From 1612123741c6a38023eaa0bbeb4e7a39cbeea8cb Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 27 Oct 2023 15:39:45 +0000 Subject: [PATCH 165/352] chore(version): v0.12.0 [skip ci] --- CHANGELOG.md | 14 ++++++++++++++ Cargo.lock | 8 ++++---- crates/cac_client/CHANGELOG.md | 6 ++++++ crates/cac_client/Cargo.toml | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- crates/superposition_client/CHANGELOG.md | 6 ++++++ crates/superposition_client/Cargo.toml | 4 ++-- 10 files changed, 48 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e003de8..18f660625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.12.0 - 2023-10-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.10.1 +- cac_client bumped to cac_client-v0.3.0 +- service-utils bumped to service-utils-v0.7.1 +- superposition_client bumped to superposition_client-v0.3.0 +### Global changes +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + ## v0.11.0 - 2023-10-25 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.6.0 diff --git a/Cargo.lock b/Cargo.lock index d35effe57..0b142ea92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,7 +577,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.2.1" +version = "0.3.0" dependencies = [ "actix-web", "chrono", @@ -703,7 +703,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.10.0" +version = "0.10.1" dependencies = [ "actix", "actix-cors", @@ -3117,7 +3117,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "actix-web", @@ -3273,7 +3273,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "derive_more", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 8d545fd0b..0fa6ae5b0 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.3.0 - 2023-10-27 +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + ## cac_client-v0.2.1 - 2023-09-20 #### Bug Fixes - PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index be68ed085..10a30082c 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.2.1" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,4 +15,4 @@ serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" log = "^0.4" json-patch = "1.0.0" -derive_more = "^0.99" \ No newline at end of file +derive_more = "^0.99" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index d336e7ba1..b4c3ac692 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.10.1 - 2023-10-27 +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.10.0 - 2023-10-25 #### Features - added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 10ecc750d..ff9aa1664 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.10.0" +version = "0.10.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index fb7ef89a9..f74ac9601 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.7.1 - 2023-10-27 +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.7.0 - 2023-10-25 #### Features - added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index dc4c3c302..ddf0f3a4b 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index b0573158a..77861b2c0 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.3.0 - 2023-10-27 +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + ## superposition_client-v0.2.0 - 2023-10-20 #### Features - PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 8fdbfddfa..4b241e758 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.2.0" +version = "0.3.0" edition = "2021" [dependencies] @@ -13,4 +13,4 @@ serde_json = "1.0.97" tokio = {version = "1.29.1", features = ["full"]} dotenv = "0.15.0" derive_more = "^0.99" -log = "^0.4" \ No newline at end of file +log = "^0.4" From cb5a92f0462fb35cad1ae19419738e31a262da56 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Tue, 31 Oct 2023 18:39:26 +0530 Subject: [PATCH 166/352] fix: x-tenant header mandate removed for OPTIONS calls - preflight requests from browsers can't send the x-tenant header --- crates/context-aware-config/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 951a9667a..b20b5218e 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -94,8 +94,8 @@ async fn main() -> Result<()> { HttpServer::new(move || { App::new() .wrap(DashboardAuth::default()) - .wrap(middlewares::cors()) .wrap(TenantMiddlewareFactory) + .wrap(middlewares::cors()) .wrap(logger::GoldenSignalFactory) .wrap(TracingLogger::::new()) .app_data(Data::new(AppState { From 9f18f5806aef1c8f433c1d15421ca40314107722 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Tue, 31 Oct 2023 14:09:54 +0000 Subject: [PATCH 167/352] chore(version): v0.12.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f660625..868423faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.12.1 - 2023-10-31 +### Package updates +- context-aware-config bumped to context-aware-config-v0.10.2 +### Global changes + +- - - + ## v0.12.0 - 2023-10-27 ### Package updates - context-aware-config bumped to context-aware-config-v0.10.1 diff --git a/Cargo.lock b/Cargo.lock index 0b142ea92..874409ba8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,7 +703,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.10.1" +version = "0.10.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index b4c3ac692..a03583693 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.10.2 - 2023-10-31 +#### Bug Fixes +- PICAF-25020 x-tenant header mandate removed for OPTIONS calls - (9ee39b5) - Ritick Madaan + +- - - + ## context-aware-config-v0.10.1 - 2023-10-27 #### Bug Fixes - fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index ff9aa1664..56d07e891 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.10.1" +version = "0.10.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From bbecd7b0939b53a12c3ed88e51a330bb6e7b9734 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Fri, 3 Nov 2023 23:12:41 +0530 Subject: [PATCH 168/352] fix: x-tenant header added for /config/resolve call in diff --- .../external/src/api/external_api/helpers.rs | 66 ++++++++++--------- makefile | 1 + 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs index 89b804a15..5386dcccd 100644 --- a/crates/external/src/api/external_api/helpers.rs +++ b/crates/external/src/api/external_api/helpers.rs @@ -1,47 +1,51 @@ use actix_web::web::Data; use experimentation_platform::{ - api::experiments::types::{VariantType, Variant}, - db::models::Experiment + api::experiments::types::{Variant, VariantType}, + db::models::Experiment, }; -use serde_json::{Value, Map}; +use serde_json::{Map, Value}; use service_utils::{ - service::types::AppState, - types as app, errors::types::Error as err, + errors::types::Error as err, service::types::AppState, types as app, }; pub fn fetch_variant_id( - experiment: &Experiment, - variant: VariantType, + experiment: &Experiment, + variant: VariantType, ) -> app::Result { - let variants = &experiment.variants; - let experiment_variants: Vec = serde_json::from_value(variants.clone()) - .map_err(|e| { - log::error!("parsing to variant type failed with err: {e}"); - err::InternalServerErr("".to_string()) - })?; + let variants = &experiment.variants; + let experiment_variants: Vec = serde_json::from_value(variants.clone()) + .map_err(|e| { + log::error!("parsing to variant type failed with err: {e}"); + err::InternalServerErr("".to_string()) + })?; - for ele in experiment_variants { - if ele.variant_type == variant { - return Ok(ele.id); + for ele in experiment_variants { + if ele.variant_type == variant { + return Ok(ele.id); + } } - } - log::info!("Failed to fetch variant {:?} id for exp {}", variant, experiment.id); - return Err(err::InternalServerErr("".to_string())); + log::info!( + "Failed to fetch variant {:?} id for exp {}", + variant, + experiment.id + ); + return Err(err::InternalServerErr("".to_string())); } pub fn get_resolved_config( - state: &Data, - dimension_ctx : &Map, + state: &Data, + dimension_ctx: &Map, ) -> app::Result { - let http_client = reqwest::Client::new(); - let url = format!("{}/config/resolve", state.cac_host); - let resp = http_client - .get(&url) - .bearer_auth(&state.admin_token) - .query(dimension_ctx) - .send() - .and_then(|mut resp| resp.json()) - .map_err(|e| err::InternalServerErr(e.to_string()))?; - Ok(resp) + let http_client = reqwest::Client::new(); + let url = format!("{}/config/resolve", state.cac_host); + let resp = http_client + .get(&url) + .bearer_auth(&state.admin_token) + .header("x-tenant", "mjos") + .query(dimension_ctx) + .send() + .and_then(|mut resp| resp.json()) + .map_err(|e| err::InternalServerErr(e.to_string()))?; + Ok(resp) } diff --git a/makefile b/makefile index 7887213c9..e9e2a1cb5 100644 --- a/makefile +++ b/makefile @@ -79,6 +79,7 @@ setup: # NOTE: The container spinned-up here is the actual container being used in development make db-init make legacy_db_setup + echo setup completed successfully!!! kill: -pkill -f target/debug/context-aware-config & From 900fbb8bd63b626f255c7037ff35395b0985a677 Mon Sep 17 00:00:00 2001 From: Ritick Madaan Date: Mon, 6 Nov 2023 12:06:06 +0530 Subject: [PATCH 169/352] fix: added external crate to cocogitto config - typing this as a fix because need a new version to be created --- cog.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cog.toml b/cog.toml index dd10ed61f..df0eea494 100644 --- a/cog.toml +++ b/cog.toml @@ -28,6 +28,7 @@ authors = [] context-aware-config = { path = "crates/context-aware-config" } experimentation-platform = { path = "crates/experimentation-platform" } service-utils = { path = "crates/service-utils" } +external = { path = "crates/external" } cac_client = { path = "crates/cac_client" } -superposition_client = { path = "crates/superposition_client" } +superposition_client = { path = "crates/superposition_client" } \ No newline at end of file From 5aafc723084fed0934ef5abce8ce2e541859ff1c Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 6 Nov 2023 07:26:16 +0000 Subject: [PATCH 170/352] chore(version): v0.13.0 [skip ci] --- CHANGELOG.md | 10 ++++++++++ crates/external/CHANGELOG.md | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 crates/external/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 868423faa..e685be1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.13.0 - 2023-11-06 +### Package updates +- external bumped to external-v0.1.0 +### Global changes +#### Bug Fixes +- PICAF-25068 added external crate to cocogitto config - (f6f60ef) - Ritick Madaan +- PICAF-25068 x-tenant header added for /config/resolve call in diff - (0e34c31) - Ritick Madaan + +- - - + ## v0.12.1 - 2023-10-31 ### Package updates - context-aware-config bumped to context-aware-config-v0.10.2 diff --git a/crates/external/CHANGELOG.md b/crates/external/CHANGELOG.md new file mode 100644 index 000000000..254def0e8 --- /dev/null +++ b/crates/external/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## external-v0.1.0 - 2023-11-06 +#### Bug Fixes +- PICAF-25068 x-tenant header added for /config/resolve call in diff - (0e34c31) - Ritick Madaan +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file From 507e08cf629a314692f62d53c88af7efea8a9f11 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Wed, 25 Oct 2023 18:02:02 +0530 Subject: [PATCH 171/352] fix: make sure envs with defaults prevent failure --- crates/context-aware-config/src/main.rs | 4 ++-- crates/service-utils/src/db/utils.rs | 4 ++-- crates/service-utils/src/helpers.rs | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index b20b5218e..1272f8529 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -22,7 +22,7 @@ use tracing_actix_web::TracingLogger; use service_utils::{ db::pgschema_manager::PgSchemaManager, db::utils::init_pool_manager, - helpers::{get_from_env_unsafe, get_pod_info}, + helpers::{get_from_env_unsafe, get_pod_info, get_from_env_or_default}, middlewares::{ app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, @@ -47,7 +47,7 @@ async fn main() -> Result<()> { let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); - let max_pool_size = get_from_env_unsafe("MAX_DB_CONNECTION_POOL_SIZE").unwrap_or(3); + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 3); let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index be966dc75..1a98b8661 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,6 +1,6 @@ use crate::aws::kms; use crate::db::pgschema_manager::{ConnectionConfig, PgSchemaManager}; -use crate::helpers::get_from_env_unsafe; +use crate::helpers::{get_from_env_unsafe, get_from_env_or_default}; use crate::service::types::AppEnv; use diesel::{ r2d2::{ConnectionManager, Pool}, @@ -24,7 +24,7 @@ pub async fn get_pool() -> Pool> { let manager: ConnectionManager = ConnectionManager::::new(db_url); Pool::builder() - .max_size(get_from_env_unsafe("MAX_DB_CONNECTION_POOL_SIZE").unwrap_or(3)) + .max_size(get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 3)) .build(manager) .expect("Error building a connection pool") } diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 3072ad17e..3a6a5a12a 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -1,6 +1,7 @@ use actix_web::{error::ErrorInternalServerError, Error}; +use log::info; use serde::de::{self, IntoDeserializer}; -use std::{env::VarError, fmt, str::FromStr}; +use std::{env::VarError, fmt::{self, Display}, str::FromStr}; //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should @@ -18,6 +19,20 @@ where }) } +pub fn get_from_env_or_default(name: &str, default: F) -> F +where + F: FromStr + Display, + ::Err: std::fmt::Debug, +{ + match std::env::var(name) { + Ok(env) => env.parse().unwrap(), + Err(err) => { + info!("{name} ENV failed to load due to {err}, using default value {default}"); + default + } + } +} + pub trait ToActixErr { fn map_err_to_internal_server( self, From eccf0912954de7a280c2893a95cd1ea2756bdb64 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra Date: Tue, 10 Oct 2023 17:34:43 +0530 Subject: [PATCH 172/352] feat: integrate authorize middleware --- .env.example | 10 +- Cargo.lock | 4 +- crates/context-aware-config/Cargo.toml | 2 +- .../src/api/context/handlers.rs | 3 +- .../src/api/default_config/handlers.rs | 6 +- .../src/api/default_config/mod.rs | 2 +- .../src/api/dimension/handlers.rs | 6 +- crates/context-aware-config/src/auth.rs | 119 + crates/context-aware-config/src/main.rs | 41 +- crates/experimentation-platform/Cargo.toml | 2 +- .../src/api/experiments/handlers.rs | 20 +- crates/external/Cargo.toml | 2 +- .../external/src/api/external_api/handlers.rs | 4 +- crates/service-utils/Cargo.toml | 2 +- crates/service-utils/src/service/types.rs | 37 +- package.json | 4 +- ...mentation-platform.postman_collection.json | 2895 +++++++++-------- .../Conclude/request.json | 5 + .../Create Experiment 2/event.prerequest.js | 2 + .../Create Experiment 2/request.json | 5 + .../Create Experiment/event.prerequest.js | 2 + .../Create Experiment/request.json | 5 + .../Update Override Keys/event.prerequest.js | 1 + .../Update Override Keys/request.json | 5 + 24 files changed, 1687 insertions(+), 1497 deletions(-) create mode 100644 crates/context-aware-config/src/auth.rs diff --git a/.env.example b/.env.example index c2a962eb5..ccf72c0bb 100644 --- a/.env.example +++ b/.env.example @@ -17,12 +17,12 @@ CAC_HOST="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" HOSTNAME="---" MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in -DASHBOARD_AUTH_ENABLED=false -TENANT_VALIDATION_ENABLED=false -AUTH_EXCLUSION_LIST="GET /context/list,GET /context/{ctx_id},GET /config/resolve,GET /config,GET /audit,GET /experiments,GET /experiments/{id}" -DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/validate/token" ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 +DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=false +TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health" \ No newline at end of file +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health" +DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" +SERVICE_NAME="CAC" diff --git a/Cargo.lock b/Cargo.lock index 874409ba8..6e9cdd1d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,8 +969,8 @@ dependencies = [ [[package]] name = "dashboard-auth" -version = "0.3.3" -source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.4.1#2162c2e2a2913d8c218990ba904acb28322543c9" +version = "0.4.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.0#22a9cadea33e89aea1f9b576a6d7b3a7226b424a" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 56d07e891..c122e6208 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -55,5 +55,5 @@ actix-http = "3.3.1" futures-util = "0.3.28" external = { path = "../external"} actix-cors = "0.6.4" -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} anyhow = { version = "1.0", default-features = false } +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 2cd34e4db..9530f3653 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -25,7 +25,7 @@ use actix_web::{ }; use anyhow::anyhow; use chrono::Utc; -use dashboard_auth::{middleware::acl, types::User}; +use dashboard_auth::types::User; use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, @@ -38,7 +38,6 @@ use service_utils::{helpers::ToActixErr, service::types::DbConnection}; pub fn endpoints() -> Scope { Scope::new("") - .guard(acl([("mjos_manager".into(), "RW".into())])) .service(put_handler) .service(move_handler) .service(delete_context) diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 614bb067f..120d78e0a 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -9,16 +9,14 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::{middleware::acl, types::User}; +use dashboard_auth::types::User; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; use service_utils::service::types::{AppState, DbConnection}; pub fn endpoints() -> Scope { - Scope::new("") - .guard(acl([("mjos_manager".into(), "RW".into())])) - .service(create) + Scope::new("").service(create) } #[put("/{key}")] diff --git a/crates/context-aware-config/src/api/default_config/mod.rs b/crates/context-aware-config/src/api/default_config/mod.rs index ebe17b924..2e277d411 100644 --- a/crates/context-aware-config/src/api/default_config/mod.rs +++ b/crates/context-aware-config/src/api/default_config/mod.rs @@ -1,3 +1,3 @@ mod handlers; mod types; -pub use handlers::endpoints; +pub use handlers::endpoints; \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 568c89df0..6e6639606 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -9,15 +9,13 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::{middleware::acl, types::User}; +use dashboard_auth::types::User; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use service_utils::service::types::{AppState, DbConnection}; pub fn endpoints() -> Scope { - Scope::new("") - .guard(acl([("mjos_manager".into(), "RW".into())])) - .service(create) + Scope::new("").service(create) } #[put("")] diff --git a/crates/context-aware-config/src/auth.rs b/crates/context-aware-config/src/auth.rs new file mode 100644 index 000000000..967a7ef7c --- /dev/null +++ b/crates/context-aware-config/src/auth.rs @@ -0,0 +1,119 @@ +pub mod experiments { + use dashboard_auth::types::AuthenticatedRoute; + + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "POST::/experiments", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::/experiments/{experiment_id}/conclude", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::/experiments/{id}/ramp", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::/experiments/{id}/stabilize", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::/experiments/{id}/revert", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::/experiments/{id}/overrides", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} + +pub mod contexts { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "PUT::/context", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::/context/move/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "DELETE::/context/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::/context/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::/context/bulk-operations", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} + +pub mod default_config { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([( + "PUT::/default-config/{key}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + )]) + } +} + +pub mod dimension { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([( + "PUT::/dimension", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + )]) + } +} diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 1272f8529..080db3d5a 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -1,4 +1,5 @@ mod api; +mod auth; mod db; mod helpers; mod logger; @@ -7,7 +8,10 @@ mod middlewares; use crate::middlewares::audit_response_header::{AuditHeader, TableName}; use actix_web::{web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use api::*; -use dashboard_auth::middleware::DashboardAuth; +use dashboard_auth::{ + middleware::DashboardAuth, + types::{AuthenticatedRoute, AuthenticatedRouteList}, +}; use dotenv; use experimentation_platform::api::*; use helpers::{get_default_config_validation_schema, get_meta_schema}; @@ -24,8 +28,7 @@ use service_utils::{ db::utils::init_pool_manager, helpers::{get_from_env_unsafe, get_pod_info, get_from_env_or_default}, middlewares::{ - app_scope::AppExecutionScopeMiddlewareFactory, - tenant::TenantMiddlewareFactory, + app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, }, service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, }; @@ -57,11 +60,12 @@ async fn main() -> Result<()> { .split(",") .map(|tenant| tenant.to_string()) .collect::>(); - let tenant_middleware_exclusion_list = get_from_env_unsafe::("TENANT_MIDDLEWARE_EXCLUSION_LIST") - .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") - .split(",") - .map(String::from) - .collect::>(); + let tenant_middleware_exclusion_list = + get_from_env_unsafe::("TENANT_MIDDLEWARE_EXCLUSION_LIST") + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(",") + .map(String::from) + .collect::>(); let string_to_int = |s: &String| -> i32 { s.chars() @@ -93,7 +97,7 @@ async fn main() -> Result<()> { HttpServer::new(move || { App::new() - .wrap(DashboardAuth::default()) + .wrap(DashboardAuth::default(authenticated_routes())) .wrap(TenantMiddlewareFactory) .wrap(middlewares::cors()) .wrap(logger::GoldenSignalFactory) @@ -122,7 +126,8 @@ async fn main() -> Result<()> { app_env: app_env.to_owned(), enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), tenants: tenants.to_owned(), - tenant_middleware_exclusion_list: tenant_middleware_exclusion_list.to_owned(), + tenant_middleware_exclusion_list: tenant_middleware_exclusion_list + .to_owned(), })) .wrap( actix_web::middleware::DefaultHeaders::new() @@ -162,8 +167,9 @@ async fn main() -> Result<()> { .service(audit_log::endpoints()), ) .service( - external::endpoints(experiments::endpoints(scope("/experiments"))) - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION)), + external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), ) }) .bind(("0.0.0.0", 8080))? @@ -173,4 +179,13 @@ async fn main() -> Result<()> { )) .run() .await -} \ No newline at end of file +} + +fn authenticated_routes() -> AuthenticatedRouteList { + let mut route_vector: Vec<(&str, AuthenticatedRoute)> = Vec::new(); + route_vector.append(&mut auth::contexts::authenticated_routes()); + route_vector.append(&mut auth::default_config::authenticated_routes()); + route_vector.append(&mut auth::dimension::authenticated_routes()); + route_vector.append(&mut auth::experiments::authenticated_routes()); + AuthenticatedRouteList::from(route_vector) +} diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index f0c0a2331..2722d28dc 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -32,4 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 74319e215..9c15362b0 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -6,10 +6,7 @@ use actix_web::{ HttpRequest, HttpResponse, Scope, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use dashboard_auth::{ - middleware::acl, - types::{Tenant, User}, -}; +use dashboard_auth::types::User; use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, @@ -17,7 +14,7 @@ use diesel::{ use service_utils::{ errors::types::{Error as err, ErrorResponse}, - service::types::{AppState, DbConnection}, + service::types::{AppState, DbConnection, Tenant}, types as app, }; @@ -27,15 +24,15 @@ use super::{ check_variants_override_coverage, validate_experiment, validate_override_keys, }, types::{ - ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextMoveReq, - ContextPutReq, ContextPutResp, ExperimentCreateRequest, ExperimentCreateResponse, - ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, - RampRequest, Variant, AuditQueryFilters + AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, + ContextMoveReq, ContextPutReq, ContextPutResp, ExperimentCreateRequest, + ExperimentCreateResponse, ExperimentResponse, ExperimentsResponse, ListFilters, + OverrideKeysUpdateRequest, RampRequest, Variant, }, }; use crate::{ - db::models::{Experiment, ExperimentStatusType, EventLog}, + db::models::{EventLog, Experiment, ExperimentStatusType}, db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, }; @@ -43,7 +40,6 @@ use serde_json::{json, Map, Value}; pub fn endpoints(scope: Scope) -> Scope { scope - .guard(acl([("mjos_manager".into(), "RW".into())])) .service(get_audit_logs) .service(create) .service(conclude_handler) @@ -715,4 +711,4 @@ async fn get_audit_logs( "total_pages": total_pages, "data": logs }))) -} \ No newline at end of file +} diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index cee048090..ed8a51854 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -22,4 +22,4 @@ chrono = { version = "0.4", features = ["serde"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.9.11"} experimentation-platform = { path = "../experimentation-platform"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} \ No newline at end of file diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index 3ed91e5a0..570813e57 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -3,7 +3,7 @@ use actix_web::{ web::{self, Data, Json}, Scope, }; -use dashboard_auth::types::{Tenant, User}; +use dashboard_auth::types::User; use crate::api::external_api::{ helpers::{fetch_variant_id, get_resolved_config}, @@ -19,7 +19,7 @@ use experimentation_platform::{ }; use serde_json::Value; use service_utils::{ - service::types::{AppState, DbConnection}, + service::types::{AppState, DbConnection, Tenant}, types as app, }; diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index ddf0f3a4b..c7513ef1f 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -31,4 +31,4 @@ serde_json = {version = "1.0"} strum_macros = "^0.24" strum = {version = "^0.24"} derive_more = "^0.99" -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.4.1"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index da609f647..579fd9801 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -1,6 +1,7 @@ use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager}; use derive_more::{Deref, DerefMut}; use jsonschema::JSONSchema; +use serde_json::json; use std::{ collections::HashSet, @@ -65,7 +66,7 @@ impl FromRequest for AppScope { fn from_request( req: &actix_web::HttpRequest, - _: &mut actix_web::dev::Payload + _: &mut actix_web::dev::Payload, ) -> Self::Future { let scope = req.extensions().get::().cloned(); let result = match scope { @@ -97,9 +98,7 @@ impl AppExecutionNamespace { tenant, scope, ) { - (false, _, _, _) => { - Ok(AppExecutionNamespace("cac_v1".to_string())) - }, + (false, _, _, _) => Ok(AppExecutionNamespace("cac_v1".to_string())), (true, _, Some(t), Some(s)) => Ok(AppExecutionNamespace(format!( "{}_{}", t.as_str(), @@ -133,7 +132,6 @@ impl FromRequest for AppExecutionNamespace { } } - #[derive(Deref, DerefMut, Clone, Debug)] pub struct Tenant(pub String); impl FromRequest for Tenant { @@ -147,7 +145,24 @@ impl FromRequest for Tenant { let tenant = req.extensions().get::().cloned(); let result = match tenant { Some(v) => Ok(v), - None => Err(error::ErrorInternalServerError("tenant not set")), + None => { + let app_state = match req.app_data::>() { + Some(val) => val, + None => { + log::error!("app state not set"); + return ready(Err(error::ErrorInternalServerError(json!({ + "message": "an unknown error occurred with the app. Please contact an admin" + })))); + } + }; + if app_state.enable_tenant_and_scope { + Err(error::ErrorInternalServerError(json!({ + "message": "tenant was not set. Please ensure you are passing in the x-tenant header" + }))) + } else { + Ok(Tenant("mjos".into())) + } + } }; ready(result) } @@ -165,13 +180,17 @@ impl FromRequest for DbConnection { ) -> Self::Future { let namespace = match AppExecutionNamespace::from_request_sync(req) { Ok(val) => val.as_str().to_string(), - Err(e) => { return ready(Err(e)); } + Err(e) => { + return ready(Err(e)); + } }; let app_state = match req.app_data::>() { Some(state) => state, None => { - log::info!("DbConnection-FromRequest: Unable to get app_data from request"); + log::info!( + "DbConnection-FromRequest: Unable to get app_data from request" + ); return ready(Err(error::ErrorInternalServerError(""))); } }; @@ -186,4 +205,4 @@ impl FromRequest for DbConnection { ready(result) } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9507e287d..5453043a2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "private": true, "description": "This is just to run automated newman tests for this service", "scripts": { - "test": "./node_modules/.bin/newman dir-run ./postman/cac && ./node_modules/.bin/newman dir-run -e postman/experiment-platform-local-env.postman_environment.json ./postman/experimentation-platform " + "test": "./node_modules/.bin/newman dir-run ./postman/cac && ./node_modules/.bin/newman dir-run -e postman/experiment-platform-local-env.postman_environment.json ./postman/experimentation-platform ", + "load_exp_tests": "npx newman dir-import postman/experimentation-platform -o postman/experimentation-platform.postman_collection.json", + "load_cac_tests": "npx newman dir-import postman/cac -o postman/cac.postman_collection.json" }, "devDependencies": { "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json index 14faf2d9b..a82e81b5c 100644 --- a/postman/experimentation-platform.postman_collection.json +++ b/postman/experimentation-platform.postman_collection.json @@ -1,1439 +1,1458 @@ { - "info": { - "_postman_id": "c7f42ca5-e0a2-4c26-a349-8de8c54c2cf1", - "name": "experimentation-platform", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "20159828" - }, - "item": [ - { - "name": "Create Experiment", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "", - "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", - " const getRequest = {", - " url: `${host}/context/${context_id}`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " ", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch context\");", - " throw error;", - " }", - "", - " const context = response.json();", - "", - " /*********** checking contexts created in CAC **********/;", - " ", - "", - " const variant_override_id = context.override_id;", - " const varaint_context = context.value;", - " const variant_override = context.override;", - "", - " console.log(\"Testing variant override id\");", - " console.log(\"Override from CAC: \\n\", variant_override_id);", - " console.log(\"Expected Context: \\n\", expected_override_id);", - " pm.expect(variant_override_id).to.be.eq(expected_override_id);", - "", - " console.log(\"Testing variant override\");", - " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", - " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", - "", - " console.log(\"Testing variant context\");", - " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", - " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", - " });", - "}", - "", - "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/experiments/${experiment_id}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch experiment\");", - " throw error;", - " }", - " ", - " const experiment = response.json();", - "", - " const context = experiment.context;", - " console.log(\"Testing Context of Experiment\");", - " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", - " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", - " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", - "", - " const variants = experiment.variants;", - " for(const variant of variants) {", - " const variant_id = variant.id;", - "", - " console.log(`TESTING variant: ${variant_id}`);", - "", - " // check if the variant present in the expected_variants", - " const variant_cpy = JSON.parse(JSON.stringify(variant));", - " delete variant_cpy.override_id;", - " delete variant_cpy.context_id;", - "", - " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", - " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", - " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", - " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", - "", - " /*********/", - "", - " const expected_context_id = variant.context_id;", - " const expected_override_id = variant.override_id;", - " const expected_override = variant.overrides;", - " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", - " ", - " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", - " }", - " });", - "}", - "", - "// check experiment creation in experiment", - "pm.test(\"200 OK\", function () {", - " const response = pm.response.json();", - " const experiment_id = response.experiment_id;", - " ", - " pm.environment.set(\"experiment_id\", experiment_id);", - " pm.response.to.have.status(200);", - "});", - "", - "", - "// check for contexts in CAC", - "pm.test(\"Test created contexts\", function() {", - " const response = pm.response.json();", - " const experiment_id = response.experiment_id;", - "", - "", - " const expected_context = {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac1\"", - " ]", - " }", - " ]", - " };", - " const expected_varaints = [", - " {", - " \"id\": `${experiment_id}-control`,", - " \"overrides\": {", - " \"pmTestKey1\": \"value1-control\",", - " \"pmTestKey2\": \"value1-control\"", - " },", - " \"variant_type\": \"CONTROL\"", - " },", - " {", - " \"id\": `${experiment_id}-test1`,", - " \"overrides\": {", - " \"pmTestKey1\": \"value2-test\",", - " \"pmTestKey2\": \"value2-test\"", - " },", - " \"variant_type\": \"EXPERIMENTAL\"", - " }", - " ];", - " const expected_variant_contexts = [", - " {", - " \"vid\": `${experiment_id}-control`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac1\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-control`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " },", - " {", - " \"vid\": `${experiment_id}-test1`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac1\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-test1`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " }", - " ];", - "", - " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", - "});" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "function create_default_config_keys() {", - " let keys = [", - " `pmTestKey1`,", - " `pmTestKey2`", - " ];", - "", - " for (const key of keys) {", - " const options = {", - " 'method': 'PUT',", - " 'url': `${host}/default-config/${key}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " },", - " \"body\": {", - " \"mode\": \"raw\",", - " \"raw\": JSON.stringify({", - " \"value\": \"value1\",", - " \"schema\": {", - " \"type\": \"string\",", - " \"pattern\": \".*\"", - " }", - " })", - " }", - " };", - " console.log(options);", - " pm.sendRequest(options, function (error, response) {", - " if (error) {", - " console.log(`Error creating default-config key: ${key}`);", - " console.log(error);", - " return;", - " };", - " console.log(`Created default-config key: ${key}`);", - " });", - " }", - "}", - "", - "function create_dimensions() {", - " const dimensions = [", - " {name: \"os\", priority: 10, schema: { type: \"string\", enum: [\"android\", \"ios\", \"web\"] }},", - " {name: \"client\", priority: 100, schema: { type: \"string\", pattern: \".*\" }},", - " {name: \"variantIds\", priority: 1000, schema: { type: \"string\", pattern: \".*\" }}", - " ];", - "", - " for (const dimension of dimensions) {", - " const options = {", - " 'method': 'PUT',", - " 'url': `${host}/dimension`,", - " 'header': {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " },", - " \"body\": {", - " \"mode\": \"raw\",", - " \"raw\": JSON.stringify({", - " \"dimension\": dimension.name,", - " \"priority\": dimension.priority,", - " \"schema\": dimension.schema", - " })", - " }", - " };", - " pm.sendRequest(options, function (error, response) {", - " if (error) {", - " console.log(`Error creating dimension: ${dimension.name}`);", - " console.log(error);", - " return;", - " }", - " console.log(`Created dimension: ${dimension.name}`);", - " });", - " }", - "}", - "", - "create_default_config_keys();", - "create_dimensions();" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"experiment-1\",\n \"override_keys\": [\"pmTestKey1\", \"pmTestKey2\"],\n \"traffic_percentage\": 10,\n \"context\": {\n \"and\": [\n {\n \"==\": [\n {\n \"var\": \"os\"\n },\n \"ios\"\n ]\n },\n {\n \"==\": [\n {\n \"var\": \"client\"\n },\n \"testClientCac1\"\n ]\n }\n ]\n },\n \"variants\": [\n {\n \"id\": \"control\",\n \"variant_type\": \"CONTROL\",\n \"overrides\": {\n \"pmTestKey1\": \"value1-control\",\n \"pmTestKey2\": \"value1-control\"\n }\n },\n {\n \"id\": \"test1\",\n \"variant_type\": \"EXPERIMENTAL\",\n \"overrides\": {\n \"pmTestKey1\": \"value2-test\",\n \"pmTestKey2\": \"value2-test\"\n }\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{host}}/experiments", - "host": [ - "{{host}}" - ], - "path": [ - "experiments" - ] - } - }, - "response": [] - }, - { - "name": "Get Experiment", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 OK\", function() {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{host}}/experiments/{{experiment_id}}", - "host": [ - "{{host}}" - ], - "path": [ - "experiments", - "{{experiment_id}}" - ] - } - }, - "response": [] - }, - { - "name": "Ramp", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/experiments/${experiment_id}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch experiment\");", - " throw error;", - " }", - " ", - " const experiment = response.json();", - " console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`);", - " pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage);", - " });", - "}", - "", - "// check experiment creation in experiment", - "pm.test(\"200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "", - "// check for contexts in CAC", - "pm.test(\"Test traffic percentage\", function() {", - " const experiment_id = pm.environment.get(\"experiment_id\");", - "", - " fetch_experiment_n_test(experiment_id, 46);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"traffic_percentage\": 46\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{host}}/experiments/{{experiment_id}}/ramp", - "host": [ - "{{host}}" - ], - "path": [ - "experiments", - "{{experiment_id}}", - "ramp" - ] - } - }, - "response": [] - }, - { - "name": "Conclude", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "const experiment_id = pm.environment.get(\"experiment_id\");", - "", - "function fetch_config_n_test(variants, winner_variant_id) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/config`,", - " 'header': {", - " 'Authorization': `Bearer ${token}`,", - " 'Contet-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch config\");", - " throw error;", - " }", - "", - " const config = response.json();", - " const contexts = config.contexts;", - " const overrides = config.overrides;", - "", - " const winner_variant = variants.find(variant => variant.id === winner_variant_id);", - " const winner_variant_override_id = winner_variant.override_id;", - " ", - " // there should be only one context with the winner variant override id", - " const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id));", - " console.log(\"Context with winner variant override\");", - " console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4));", - " pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1);", - "", - " // there should be 0 contexts with variant as a dimension", - " const contexts_with_variant_dim = contexts", - " .filter(", - " (context) => ", - " context.condition.and", - " ?.map(", - " (condition) => ", - " Object.keys(condition)", - " .map((k) => condition[k][0].var === \"variant\")", - " .reduce((p, c) => p || c, false))", - " .reduce((p, c) => p || c, false)", - " );", - " pm.expect(contexts_with_variant_dim.length).to.be.eq(0);", - "", - " // checking if winner override exists and is same as the expected override", - " const winner_variant_context = contexts_with_winner_variant_override[0]; ", - " pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1);", - " pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id));", - "", - " // checking if all the discarded overrides are removed", - " const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id);", - " const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id);", - " const available_overrides = Object.keys(overrides);", - " for(const ao of available_overrides) {", - " pm.expect(discarded_variants_override_ids).to.not.include(ao);", - " }", - " });", - "}", - "", - "function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/experiments/${experiment_id}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch experiment\");", - " throw error;", - " }", - " ", - " const experiment = response.json();", - "", - " const status = experiment.status;", - " pm.expect(status).to.be.eq(expected_status);", - "", - " const variants = experiment.variants;", - " fetch_config_n_test(variants, winner_variant_id);", - " });", - "}", - "", - "pm.test(\"200 OK\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Conclude correctness\", function() {", - " const winner_variant_id = `${experiment_id}-control`;", - " fetch_experiment_n_test(experiment_id, winner_variant_id, \"CONCLUDED\")", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"chosen_variant\": \"{{experiment_id}}-control\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{host}}/experiments/{{experiment_id}}/conclude", - "host": [ - "{{host}}" - ], - "path": [ - "experiments", - "{{experiment_id}}", - "conclude" - ] - } - }, - "response": [] - }, - { - "name": "Create Experiment 2", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "function create_default_config_keys() {", - " let keys = [", - " `pmTestKey4`,", - " `pmTestKey3`", - " ];", - "", - " for (const key of keys) {", - " const options = {", - " 'method': 'PUT',", - " 'url': `${host}/default-config/${key}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " },", - " \"body\": {", - " \"mode\": \"raw\",", - " \"raw\": JSON.stringify({", - " \"value\": \"value1\",", - " \"schema\": {", - " \"type\": \"string\",", - " \"pattern\": \".*\"", - " }", - " })", - " }", - " };", - " console.log(options);", - " pm.sendRequest(options, function (error, response) {", - " if (error) {", - " console.log(`Error creating default-config key: ${key}`);", - " console.log(error);", - " return;", - " };", - " console.log(`Created default-config key: ${key}`);", - " });", - " }", - "}", - "", - "function create_dimensions() {", - " const dimensions = [", - " {name: \"os\", priority: 10, type: \"STRING\"},", - " {name: \"client\", priority: 100, type: \"STRING\"},", - " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", - " ];", - "", - " for (const dimension of dimensions) {", - " const options = {", - " 'method': 'PUT',", - " 'url': `${host}/dimension`,", - " 'header': {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " },", - " \"body\": {", - " \"mode\": \"raw\",", - " \"raw\": JSON.stringify({", - " \"dimension\": dimension.name,", - " \"priority\": dimension.priority,", - " \"type\": dimension.type", - " })", - " }", - " };", - " pm.sendRequest(options, function (error, response) {", - " if (error) {", - " console.log(`Error creating dimension: ${dimension.name}`);", - " console.log(error);", - " return;", - " }", - " console.log(`Created dimension: ${dimension.name}`);", - " });", - " }", - "}", - "", - "create_dimensions();", - "create_default_config_keys();" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "", - "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", - " const getRequest = {", - " url: `${host}/context/${context_id}`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " ", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch context\");", - " throw error;", - " }", - "", - " const context = response.json();", - "", - " /*********** checking contexts created in CAC **********/;", - " ", - "", - " const variant_override_id = context.override_id;", - " const varaint_context = context.value;", - " const variant_override = context.override;", - "", - " console.log(\"Testing variant override id\");", - " console.log(\"Override from CAC: \\n\", variant_override_id);", - " console.log(\"Expected Context: \\n\", expected_override_id);", - " pm.expect(variant_override_id).to.be.eq(expected_override_id);", - "", - " console.log(\"Testing variant override\");", - " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", - " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", - "", - " console.log(\"Testing variant context\");", - " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", - " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", - " });", - "}", - "", - "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/experiments/${experiment_id}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch experiment\");", - " throw error;", - " }", - " ", - " const experiment = response.json();", - "", - " const context = experiment.context;", - " console.log(\"Testing Context of Experiment\");", - " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", - " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", - " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", - "", - " const variants = experiment.variants;", - " for(const variant of variants) {", - " const variant_id = variant.id;", - "", - " console.log(`TESTING variant: ${variant_id}`);", - "", - " // check if the variant present in the expected_variants", - " const variant_cpy = JSON.parse(JSON.stringify(variant));", - " delete variant_cpy.override_id;", - " delete variant_cpy.context_id;", - "", - " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", - " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", - " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", - " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", - "", - " /*********/", - "", - " const expected_context_id = variant.context_id;", - " const expected_override_id = variant.override_id;", - " const expected_override = variant.overrides;", - " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", - " ", - " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", - " }", - " });", - "}", - "", - "// check experiment creation in experiment", - "pm.test(\"200 OK\", function () {", - " const response = pm.response.json();", - " const experiment_id = response.experiment_id;", - " ", - " pm.environment.set(\"experiment_id\", experiment_id);", - " pm.response.to.have.status(200);", - "});", - "", - "", - "// check for contexts in CAC", - "pm.test(\"Test created contexts\", function() {", - " const response = pm.response.json();", - " const experiment_id = response.experiment_id;", - "", - "", - " const expected_context = {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac02\"", - " ]", - " }", - " ]", - " };", - " const expected_varaints = [", - " {", - " \"id\": `${experiment_id}-control`,", - " \"overrides\": {", - " \"pmTestKey3\": \"value3-control\",", - " \"pmTestKey4\": \"value3-control\"", - " },", - " \"variant_type\": \"CONTROL\"", - " },", - " {", - " \"id\": `${experiment_id}-test1`,", - " \"overrides\": {", - " \"pmTestKey3\": \"value4-test\",", - " \"pmTestKey4\": \"value4-test\"", - " },", - " \"variant_type\": \"EXPERIMENTAL\"", - " }", - " ];", - " const expected_variant_contexts = [", - " {", - " \"vid\": `${experiment_id}-control`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac02\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-control`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " },", - " {", - " \"vid\": `${experiment_id}-test1`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac02\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-test1`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " }", - " ];", - "", - " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"experiment-1\",\n \"override_keys\": [\"pmTestKey3\", \"pmTestKey4\"],\n \"traffic_percentage\": 10,\n \"context\": {\n \"and\": [\n {\n \"==\": [\n {\n \"var\": \"os\"\n },\n \"ios\"\n ]\n },\n {\n \"==\": [\n {\n \"var\": \"client\"\n },\n \"testClientCac02\"\n ]\n }\n ]\n },\n \"variants\": [\n {\n \"id\": \"control\",\n \"variant_type\": \"CONTROL\",\n \"overrides\": {\n \"pmTestKey3\": \"value3-control\",\n \"pmTestKey4\": \"value3-control\"\n }\n },\n {\n \"id\": \"test1\",\n \"variant_type\": \"EXPERIMENTAL\",\n \"overrides\": {\n \"pmTestKey3\": \"value4-test\",\n \"pmTestKey4\": \"value4-test\"\n }\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{host}}/experiments", - "host": [ - "{{host}}" - ], - "path": [ - "experiments" - ] - } - }, - "response": [] - }, - { - "name": "Update Override Keys", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "function create_default_config_keys() {", - " let keys = [", - " `pmTestKey1972`,", - " `pmTestKey1999`", - " ];", - "", - " for (const key of keys) {", - " const options = {", - " 'method': 'PUT',", - " 'url': `${host}/default-config/${key}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " },", - " \"body\": {", - " \"mode\": \"raw\",", - " \"raw\": JSON.stringify({", - " \"value\": \"value1\",", - " \"schema\": {", - " \"type\": \"string\",", - " \"pattern\": \".*\"", - " }", - " })", - " }", - " };", - " console.log(options);", - " pm.sendRequest(options, function (error, response) {", - " if (error) {", - " console.log(`Error creating default-config key: ${key}`);", - " console.log(error);", - " return;", - " };", - " console.log(`Created default-config key: ${key}`);", - " });", - " }", - "}", - "", - "create_default_config_keys()" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "const host = pm.environment.get(\"host\");", - "const token = pm.environment.get(\"token\");", - "", - "", - "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", - " const getRequest = {", - " url: `${host}/context/${context_id}`,", - " method: 'GET',", - " header: {", - " 'Content-Type': 'application/json',", - " }", - " };", - "", - " ", - " pm.sendRequest(getRequest, (error, response) => {", - " if(error) {", - " console.log(\"Failed to fetch context\");", - " throw error;", - " }", - "", - " const context = response.json();", - "", - " /*********** checking contexts created in CAC **********/;", - " ", - "", - " const variant_override_id = context.override_id;", - " const varaint_context = context.value;", - " const variant_override = context.override;", - "", - " console.log(\"Testing variant override id\");", - " console.log(\"Override from CAC: \\n\", variant_override_id);", - " console.log(\"Expected Context: \\n\", expected_override_id);", - " pm.expect(variant_override_id).to.be.eq(expected_override_id);", - "", - " console.log(\"Testing variant override\");", - " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", - " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", - "", - " console.log(\"Testing variant context\");", - " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", - " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", - " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", - " });", - "}", - "", - "function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) {", - " const options = {", - " 'method': 'GET',", - " 'url': `${host}/experiments/${experiment_id}`,", - " \"header\": {", - " 'Authorization': `Bearer ${token}`,", - " 'Content-Type': 'application/json'", - " }", - " };", - "", - " pm.sendRequest(options, function(error, response) {", - " if(error) {", - " console.log(\"Failed to fetch experiment\");", - " throw error;", - " }", - " ", - " const experiment = response.json();", - "", - " const variants = experiment.variants;", - " for(const variant of variants) {", - " const variant_id = variant.id;", - "", - " console.log(`TESTING variant: ${variant_id}`);", - "", - " // check if the variant present in the expected_variants", - " const variant_cpy = JSON.parse(JSON.stringify(variant));", - " delete variant_cpy.override_id;", - " delete variant_cpy.context_id;", - "", - " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", - " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", - " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", - " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", - "", - " /*********/", - "", - " const expected_context_id = variant.context_id;", - " const expected_override_id = variant.override_id;", - " const expected_override = variant.overrides;", - " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", - " ", - " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", - " }", - " });", - "}", - "", - "// check experiment creation in experiment", - "pm.test(\"200 OK\", function () {", - " const response = pm.response.json();", - " const experiment_id = response.experiment_id;", - " ", - " pm.environment.set(\"experiment_id\", experiment_id);", - " pm.response.to.have.status(200);", - "});", - "", - "", - "// check for contexts in CAC", - "pm.test(\"Test updated experiment\", function() {", - " const response = pm.response.json();", - " const experiment_id = response.id;", - "", - " const expected_varaints = [", - " {", - " \"id\": `${experiment_id}-control`,", - " \"overrides\": {", - " \"pmTestKey1972\": \"value-7910-an-control\",", - " \"pmTestKey1999\": \"value-6910-an-control\"", - " },", - " \"variant_type\": \"CONTROL\"", - " },", - " {", - " \"id\": `${experiment_id}-test1`,", - " \"overrides\": {", - " \"pmTestKey1972\": \"value-7920-an-test\",", - " \"pmTestKey1999\": \"value-6930-an-test\"", - " },", - " \"variant_type\": \"EXPERIMENTAL\"", - " }", - " ];", - " const expected_variant_contexts = [", - " {", - " \"vid\": `${experiment_id}-control`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac02\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-control`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " },", - " {", - " \"vid\": `${experiment_id}-test1`,", - " \"context\": {", - " \"and\": [", - " {", - " \"==\": [", - " {", - " \"var\": \"os\"", - " },", - " \"ios\"", - " ]", - " },", - " {", - " \"==\": [", - " {", - " \"var\": \"client\"", - " },", - " \"testClientCac02\"", - " ]", - " },", - " {", - " \"in\": [", - " `${experiment_id}-test1`,", - " {", - " \"var\": \"variantIds\"", - " }", - " ]", - " }", - " ]", - " }", - " }", - " ];", - "", - " fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"override_keys\": [\"pmTestKey1972\", \"pmTestKey1999\"],\n \"variants\": [\n {\n \"id\": \"{{experiment_id}}-control\",\n \"overrides\": {\n \"pmTestKey1972\": \"value-7910-an-control\",\n \"pmTestKey1999\": \"value-6910-an-control\"\n }\n },\n {\n \"id\": \"{{experiment_id}}-test1\",\n \"overrides\": {\n \"pmTestKey1972\": \"value-7920-an-test\",\n \"pmTestKey1999\": \"value-6930-an-test\"\n }\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{host}}/experiments/{{experiment_id}}/overrides", - "host": [ - "{{host}}" - ], - "path": [ - "experiments", - "{{experiment_id}}", - "overrides" - ] - } - }, - "response": [] - }, - { - "name": "List experiments [No If-Modified-Since]", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}" - } - ], - "url": { - "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", - "host": [ - "{{host}}" - ], - "path": [ - "experiments" - ], - "query": [ - { - "key": "from_date", - "value": "2023-01-01%2000%3A00%3A00%20UTC" - }, - { - "key": "to_date", - "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" - }, - { - "key": "page", - "value": "1" - }, - { - "key": "count", - "value": "100" - }, - { - "key": "status", - "value": "CREATED,INPROGRESS" - } - ] - } - }, - "response": [] - }, - { - "name": "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200 check\", function() {", - " pm.response.to.have.status(200);", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}" - }, - { - "key": "If-Modified-Since", - "value": "Thu, 01 Jan 1970 00:00:00 +0000", - "type": "default" - } - ], - "url": { - "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", - "host": [ - "{{host}}" - ], - "path": [ - "experiments" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "count", - "value": "100" - }, - { - "key": "status", - "value": "CREATED,INPROGRESS,CONCLUDED" - } - ] - } - }, - "response": [] - }, - { - "name": "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"304 check\", function() {", - " pm.response.to.have.status(304);", - "})" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{token}}" - }, - { - "key": "If-Modified-Since", - "value": "Wed, 01 Jan 2070 00:00:00 +0000", - "type": "default" - } - ], - "url": { - "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", - "host": [ - "{{host}}" - ], - "path": [ - "experiments" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "count", - "value": "100" - }, - { - "key": "status", - "value": "CREATED,INPROGRESS" - } - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "key": "host", - "value": "http://localhost:8080", - "type": "default" - }, - { - "key": "token", - "value": "12345678", - "type": "default" - } - ] -} \ No newline at end of file + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Create Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value1-control\",", + " \"pmTestKey2\": \"value1-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value2-test\",", + " \"pmTestKey2\": \"value2-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey1`,", + " `pmTestKey2`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, schema: { type: \"string\", enum: [\"android\", \"ios\", \"web\"] }},", + " {name: \"client\", priority: 100, schema: { type: \"string\", pattern: \".*\" }},", + " {name: \"variantIds\", priority: 1000, schema: { type: \"string\", pattern: \".*\" }}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"schema\": dimension.schema", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_default_config_keys();", + "create_dimensions();" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"name\":\"experiment-1\",\"override_keys\":[\"pmTestKey1\",\"pmTestKey2\"],\"traffic_percentage\":10,\"context\":{\"and\":[{\"==\":[{\"var\":\"os\"},\"ios\"]},{\"==\":[{\"var\":\"client\"},\"testClientCac1\"]}]},\"variants\":[{\"id\":\"control\",\"variant_type\":\"CONTROL\",\"overrides\":{\"pmTestKey1\":\"value1-control\",\"pmTestKey2\":\"value1-control\"}},{\"id\":\"test1\",\"variant_type\":\"EXPERIMENTAL\",\"overrides\":{\"pmTestKey1\":\"value2-test\",\"pmTestKey2\":\"value2-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Get Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Ramp", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + " console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`);", + " pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage);", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test traffic percentage\", function() {", + " const experiment_id = pm.environment.get(\"experiment_id\");", + "", + " fetch_experiment_n_test(experiment_id, 46);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"traffic_percentage\":46}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/ramp", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "ramp" + ] + } + }, + "response": [] + }, + { + "name": "Conclude", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "const experiment_id = pm.environment.get(\"experiment_id\");", + "", + "function fetch_config_n_test(variants, winner_variant_id) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/config`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'Contet-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const config = response.json();", + " const contexts = config.contexts;", + " const overrides = config.overrides;", + "", + " const winner_variant = variants.find(variant => variant.id === winner_variant_id);", + " const winner_variant_override_id = winner_variant.override_id;", + " ", + " // there should be only one context with the winner variant override id", + " const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id));", + " console.log(\"Context with winner variant override\");", + " console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4));", + " pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1);", + "", + " // there should be 0 contexts with variant as a dimension", + " const contexts_with_variant_dim = contexts", + " .filter(", + " (context) => ", + " context.condition.and", + " ?.map(", + " (condition) => ", + " Object.keys(condition)", + " .map((k) => condition[k][0].var === \"variant\")", + " .reduce((p, c) => p || c, false))", + " .reduce((p, c) => p || c, false)", + " );", + " pm.expect(contexts_with_variant_dim.length).to.be.eq(0);", + "", + " // checking if winner override exists and is same as the expected override", + " const winner_variant_context = contexts_with_winner_variant_override[0]; ", + " pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1);", + " pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id));", + "", + " // checking if all the discarded overrides are removed", + " const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id);", + " const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id);", + " const available_overrides = Object.keys(overrides);", + " for(const ao of available_overrides) {", + " pm.expect(discarded_variants_override_ids).to.not.include(ao);", + " }", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const status = experiment.status;", + " pm.expect(status).to.be.eq(expected_status);", + "", + " const variants = experiment.variants;", + " fetch_config_n_test(variants, winner_variant_id);", + " });", + "}", + "", + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Conclude correctness\", function() {", + " const winner_variant_id = `${experiment_id}-control`;", + " fetch_experiment_n_test(experiment_id, winner_variant_id, \"CONCLUDED\")", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"chosen_variant\":\"{{experiment_id}}-control\"}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/conclude", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "conclude" + ] + } + }, + "response": [] + }, + { + "name": "Create Experiment 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey4`,", + " `pmTestKey3`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, type: \"STRING\"},", + " {name: \"client\", priority: 100, type: \"STRING\"},", + " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"type\": dimension.type", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_dimensions();", + "create_default_config_keys();" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value3-control\",", + " \"pmTestKey4\": \"value3-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value4-test\",", + " \"pmTestKey4\": \"value4-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"name\":\"experiment-1\",\"override_keys\":[\"pmTestKey3\",\"pmTestKey4\"],\"traffic_percentage\":10,\"context\":{\"and\":[{\"==\":[{\"var\":\"os\"},\"ios\"]},{\"==\":[{\"var\":\"client\"},\"testClientCac02\"]}]},\"variants\":[{\"id\":\"control\",\"variant_type\":\"CONTROL\",\"overrides\":{\"pmTestKey3\":\"value3-control\",\"pmTestKey4\":\"value3-control\"}},{\"id\":\"test1\",\"variant_type\":\"EXPERIMENTAL\",\"overrides\":{\"pmTestKey3\":\"value4-test\",\"pmTestKey4\":\"value4-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Update Override Keys", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey1972`,", + " `pmTestKey1999`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "create_default_config_keys()" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test updated experiment\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.id;", + "", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7910-an-control\",", + " \"pmTestKey1999\": \"value-6910-an-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7920-an-test\",", + " \"pmTestKey1999\": \"value-6930-an-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"override_keys\":[\"pmTestKey1972\",\"pmTestKey1999\"],\"variants\":[{\"id\":\"{{experiment_id}}-control\",\"overrides\":{\"pmTestKey1972\":\"value-7910-an-control\",\"pmTestKey1999\":\"value-6910-an-control\"}},{\"id\":\"{{experiment_id}}-test1\",\"overrides\":{\"pmTestKey1972\":\"value-7920-an-test\",\"pmTestKey1999\":\"value-6930-an-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/overrides", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "overrides" + ] + } + }, + "response": [] + }, + { + "name": "List experiments [No If-Modified-Since]", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "from_date", + "value": "2023-01-01%2000%3A00%3A00%20UTC" + }, + { + "key": "to_date", + "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Thu, 01 Jan 1970 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS,CONCLUDED" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"304 check\", function() {", + " pm.response.to.have.status(304);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Wed, 01 Jan 2070 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + } + ], + "info": { + "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "name": "experimentation-platform", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/experimentation-platform/Conclude/request.json b/postman/experimentation-platform/Conclude/request.json index 32f60bff8..2bf875d5a 100644 --- a/postman/experimentation-platform/Conclude/request.json +++ b/postman/experimentation-platform/Conclude/request.json @@ -10,6 +10,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "text" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" } ], "body": { diff --git a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js index 8e3c7eb64..43715a7d8 100644 --- a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js +++ b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js @@ -13,6 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, + 'x-tenant': 'mjos', 'Content-Type': 'application/json' }, "body": { @@ -51,6 +52,7 @@ function create_dimensions() { 'url': `${host}/dimension`, 'header': { 'Authorization': `Bearer ${token}`, + 'x-tenant': 'mjos', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Create Experiment 2/request.json b/postman/experimentation-platform/Create Experiment 2/request.json index 889c65094..f78771300 100644 --- a/postman/experimentation-platform/Create Experiment 2/request.json +++ b/postman/experimentation-platform/Create Experiment 2/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" } ], "body": { diff --git a/postman/experimentation-platform/Create Experiment/event.prerequest.js b/postman/experimentation-platform/Create Experiment/event.prerequest.js index 633348069..516679d96 100644 --- a/postman/experimentation-platform/Create Experiment/event.prerequest.js +++ b/postman/experimentation-platform/Create Experiment/event.prerequest.js @@ -13,6 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, + 'x-tenant': 'mjos', 'Content-Type': 'application/json' }, "body": { @@ -51,6 +52,7 @@ function create_dimensions() { 'url': `${host}/dimension`, 'header': { 'Authorization': `Bearer ${token}`, + 'x-tenant': 'mjos', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Create Experiment/request.json b/postman/experimentation-platform/Create Experiment/request.json index c101d0c77..519ccb6aa 100644 --- a/postman/experimentation-platform/Create Experiment/request.json +++ b/postman/experimentation-platform/Create Experiment/request.json @@ -6,6 +6,11 @@ "value": "Bearer {{token}}", "type": "default" }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + }, { "key": "Content-Type", "value": "application/json", diff --git a/postman/experimentation-platform/Update Override Keys/event.prerequest.js b/postman/experimentation-platform/Update Override Keys/event.prerequest.js index 9b7b90d03..17f71ffcf 100644 --- a/postman/experimentation-platform/Update Override Keys/event.prerequest.js +++ b/postman/experimentation-platform/Update Override Keys/event.prerequest.js @@ -13,6 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, + 'x-tenant': 'mjos', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Update Override Keys/request.json b/postman/experimentation-platform/Update Override Keys/request.json index 502913dbd..ad47235bb 100644 --- a/postman/experimentation-platform/Update Override Keys/request.json +++ b/postman/experimentation-platform/Update Override Keys/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" } ], "body": { From 01d94aee3bb240bb65c780b9ad999ed039cc1817 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 8 Nov 2023 06:53:48 +0000 Subject: [PATCH 173/352] chore(version): v0.14.0 [skip ci] --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 8 ++++---- crates/context-aware-config/CHANGELOG.md | 8 ++++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 4 ++-- crates/external/CHANGELOG.md | 6 ++++++ crates/external/Cargo.toml | 4 ++-- crates/service-utils/CHANGELOG.md | 8 ++++++++ crates/service-utils/Cargo.toml | 2 +- 10 files changed, 50 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e685be1c9..421b45bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.14.0 - 2023-11-08 +### Package updates +- external bumped to external-v0.2.0 +- experimentation-platform bumped to experimentation-platform-v0.7.0 +- service-utils bumped to service-utils-v0.8.0 +- context-aware-config bumped to context-aware-config-v0.11.0 +### Global changes +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + ## v0.13.0 - 2023-11-06 ### Package updates - external bumped to external-v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 6e9cdd1d2..60d01d0e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,7 +703,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.10.2" +version = "0.11.0" dependencies = [ "actix", "actix-cors", @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.6.0" +version = "0.7.0" dependencies = [ "actix", "actix-web", @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "external" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix", "actix-web", @@ -3117,7 +3117,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.7.1" +version = "0.8.0" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index a03583693..9d19ade37 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.11.0 - 2023-11-08 +#### Bug Fixes +- make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + ## context-aware-config-v0.10.2 - 2023-10-31 #### Bug Fixes - PICAF-25020 x-tenant header mandate removed for OPTIONS calls - (9ee39b5) - Ritick Madaan diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index c122e6208..c64502370 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.10.2" +version = "0.11.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 00a0208c4..de7212a3c 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.7.0 - 2023-11-08 +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.6.0 - 2023-10-25 #### Features - added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 2722d28dc..1488beeed 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.6.0" +version = "0.7.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -32,4 +32,4 @@ diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chr diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} diff --git a/crates/external/CHANGELOG.md b/crates/external/CHANGELOG.md index 254def0e8..81055286f 100644 --- a/crates/external/CHANGELOG.md +++ b/crates/external/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## external-v0.2.0 - 2023-11-08 +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + ## external-v0.1.0 - 2023-11-06 #### Bug Fixes - PICAF-25068 x-tenant header added for /config/resolve call in diff - (0e34c31) - Ritick Madaan diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index ed8a51854..3668a2a09 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "external" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,4 +22,4 @@ chrono = { version = "0.4", features = ["serde"] } service-utils = { path = "../service-utils" } reqwest = {version = "0.9.11"} experimentation-platform = { path = "../experimentation-platform"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index f74ac9601..ef97406ab 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.8.0 - 2023-11-08 +#### Bug Fixes +- make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + ## service-utils-v0.7.1 - 2023-10-27 #### Bug Fixes - fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index c7513ef1f..74cf7feba 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.7.1" +version = "0.8.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8ad81c34ce3d07977f8b2ac4ffd494e9a4cef087 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Fri, 20 Oct 2023 00:40:00 +0530 Subject: [PATCH 174/352] fix: cac service to set last_modified header --- crates/cac_client/src/lib.rs | 45 +++++++++--- .../src/api/config/handlers.rs | 72 ++++++++++++++----- .../src/api/default_config/handlers.rs | 2 +- .../src/api/dimension/handlers.rs | 2 +- .../experimentation-platform/src/db/models.rs | 2 +- 5 files changed, 94 insertions(+), 29 deletions(-) diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 13bb8e630..a75d1d475 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -6,12 +6,17 @@ use actix_web::{ web::Data, }; use chrono::{DateTime, Utc}; -use reqwest::{RequestBuilder, StatusCode}; +use derive_more::{Deref, DerefMut}; +use reqwest::{RequestBuilder, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::{convert::identity, sync::{RwLock, Arc}, time::Duration, collections::HashMap}; +use std::{ + collections::HashMap, + convert::identity, + sync::{Arc, RwLock}, + time::{Duration, UNIX_EPOCH}, +}; use utils::core::MapError; -use derive_more::{Deref, DerefMut}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { @@ -40,6 +45,22 @@ fn clone_reqw(reqw: &RequestBuilder) -> Result { .ok_or_else(|| "Unable to clone reqw".to_string()) } +fn get_last_modified( + resp: &Response +) -> Option> { + resp.headers() + .get("last-modified") + .and_then(|header_val| { + let header_str = header_val.to_str().ok()?; + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc)) + .map_err(|e| { + log::error!("Failed to parse date: {e}"); + }) + .ok() + }) +} + impl Client { pub async fn new( tenant: String, @@ -55,13 +76,16 @@ impl Client { let reqwc = clone_reqw(&reqw)?; let resp = reqwc.send().await.map_err_to_string()?; + let last_modified_at = get_last_modified(&resp); let config = resp.json::().await.map_err_to_string()?; - let timestamp = Utc::now(); + let client = Client { tenant, reqw: Data::new(reqw), polling_interval, - last_modified: Data::new(RwLock::new(timestamp)), + last_modified: Data::new(RwLock::new( + last_modified_at.unwrap_or(DateTime::::from(UNIX_EPOCH)), + )), config: Data::new(RwLock::new(config)), }; if update_config_periodically { @@ -70,7 +94,7 @@ impl Client { Ok(client) } - async fn fetch(&self) -> Result { + async fn fetch(&self) -> Result { let last_modified = self.last_modified.read().map_err_to_string()?.to_rfc2822(); let reqw = clone_reqw(&self.reqw)?.header("If-Modified-Since", last_modified); let resp = reqw.send().await.map_err_to_string()?; @@ -81,15 +105,18 @@ impl Client { StatusCode::OK => log::info!("{}", format!("{} CAC: new config received, updating", self.tenant)), x => return Err(format!("{} CAC: fetch failed, status: {}", self.tenant, x)), }; - resp.json::().await.map_err_to_string() + Ok(resp) } async fn update_cac(&self) -> Result { let fetched_config = self.fetch().await?; let mut config = self.config.write().map_err_to_string()?; let mut last_modified = self.last_modified.write().map_err_to_string()?; - *config = fetched_config; - *last_modified = Utc::now(); + let last_modified_at = get_last_modified(&fetched_config); + *config = fetched_config.json::().await.map_err_to_string()?; + if let Some(val) = last_modified_at { + *last_modified = val; + } Ok(format!("{}: CAC updated successfully", self.tenant)) } diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 87146f9eb..8496132ac 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -4,11 +4,12 @@ use super::types::Config; use crate::db::schema::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; +use actix_http::header::{HeaderName, HeaderValue}; use actix_web::{ error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope, }; use cac_client::eval_cac; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; use diesel::{ dsl::max, r2d2::{ConnectionManager, PooledConnection}, @@ -21,16 +22,36 @@ pub fn endpoints() -> Scope { Scope::new("").service(get).service(get_resolved_config) } -fn is_not_modified( - req: &HttpRequest, +fn add_last_modified_header( + max_created_at: Option, + mut res: HttpResponse, +) -> actix_web::Result { + let header_name = HeaderName::from_static("last-modified"); + + if let Some(ele) = max_created_at { + let datetime_utc: DateTime = DateTime::from_utc(ele, Utc); + let value = HeaderValue::from_str(&DateTime::to_rfc2822(&datetime_utc)); + if let Ok(header_value) = value { + res.headers_mut().insert(header_name, header_value); + } + } + Ok(res) +} + +fn get_max_created_at( conn: &mut PooledConnection>, -) -> actix_web::Result { - let max_created_at: Option = event_log::event_log +) -> Result { + event_log::event_log .select(max(event_log::timestamp)) - .filter(event_log::table_name.eq("contexts")) - .first(conn) - .map_err_to_internal_server("error getting created at", Null)?; + .filter(event_log::table_name.eq_any(vec!["contexts", "default_configs"])) + .first::>(conn) + .and_then(|res| res.ok_or(diesel::result::Error::NotFound)) +} +fn is_not_modified( + max_created_at: Option, + req: &HttpRequest, +) -> anyhow::Result { let last_modified = req .headers() .get("If-Modified-Since") @@ -39,9 +60,8 @@ fn is_not_modified( DateTime::parse_from_rfc2822(header_str) .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) .ok() - }); - - Ok(max_created_at.is_some() && max_created_at < last_modified) + }).and_then(|t| t.with_nanosecond(0)); + Ok(max_created_at.is_some() && max_created_at <= last_modified) } async fn generate_cac( @@ -91,11 +111,20 @@ async fn generate_cac( async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result { let DbConnection(mut conn) = db_conn; - if is_not_modified(&req, &mut conn)? { + let max_created_at = get_max_created_at(&mut conn) + .map_err(|e| log::error!("failed to fetch max timestamp from event_log: {e}")) + .ok(); + + let is_not_modified = is_not_modified(max_created_at, &req) + .map_err(|e| log::error!("config not modified: {e}")); + + if let Ok(true) = is_not_modified { return Ok(HttpResponse::NotModified().finish()); } - Ok(HttpResponse::Ok().json(generate_cac(&mut conn).await?)) + let res = HttpResponse::Ok().json(generate_cac(&mut conn).await?); + + add_last_modified_header(max_created_at, res) } #[get("/resolve")] @@ -118,7 +147,14 @@ async fn get_resolved_config( ); } - if is_not_modified(&req, &mut conn)? { + let max_created_at = get_max_created_at(&mut conn) + .map_err(|e| log::error!("failed to fetch max timestamp from event_log : {e}")) + .ok(); + + let is_not_modified = is_not_modified(max_created_at, &req) + .map_err(|e| log::error!("config not modified: {e}")); + + if let Ok(true) = is_not_modified { return Ok(HttpResponse::NotModified().finish()); } @@ -133,7 +169,7 @@ async fn get_resolved_config( }) .collect(); - Ok(HttpResponse::Ok().json( + let response = HttpResponse::Ok().json( eval_cac( res.default_configs, &cac_client_contexts, @@ -141,5 +177,7 @@ async fn get_resolved_config( &query_params_map, ) .map_err_to_internal_server("cac eval failed", Null)?, - )) -} \ No newline at end of file + ); + + add_last_modified_header(max_created_at, response) +} diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 120d78e0a..012680c69 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -76,4 +76,4 @@ async fn create( .body("Failed to create DefaultConfig"); } } -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 6e6639606..c64d13b9a 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -74,4 +74,4 @@ async fn create( .body("Failed to create/update dimension\n"); } } -} +} \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation-platform/src/db/models.rs index a3767b208..3711c011f 100644 --- a/crates/experimentation-platform/src/db/models.rs +++ b/crates/experimentation-platform/src/db/models.rs @@ -51,4 +51,4 @@ pub struct EventLog { pub original_data: Option, pub new_data: Option, pub query: String, -} \ No newline at end of file +} From c7fae30f202a22c87effaddecf93dcc9f21b7e2b Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Sat, 7 Oct 2023 21:47:38 +0530 Subject: [PATCH 175/352] CAC Filter API --- .../src/api/config/handlers.rs | 83 +++++++++++++++++- .../src/api/config/types.rs | 2 +- .../src/api/experiments/helpers.rs | 86 +++++++++---------- .../tests/experimentation_tests.rs | 4 +- 4 files changed, 123 insertions(+), 52 deletions(-) diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 8496132ac..27cbd836c 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use super::types::Config; +use super::types::{Config, Context}; use crate::db::schema::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; @@ -15,11 +15,18 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; +use experimentation_platform::api::experiments::helpers::extract_dimensions; use serde_json::{json, Map, Value, Value::Null}; -use service_utils::{helpers::ToActixErr, service::types::DbConnection}; +use service_utils::{ + errors::types::Error as err, helpers::ToActixErr, service::types::DbConnection, + types as app, +}; pub fn endpoints() -> Scope { - Scope::new("").service(get).service(get_resolved_config) + Scope::new("") + .service(get) + .service(get_resolved_config) + .service(get_filtered_config) } fn add_last_modified_header( @@ -178,6 +185,74 @@ async fn get_resolved_config( ) .map_err_to_internal_server("cac eval failed", Null)?, ); - add_last_modified_header(max_created_at, response) } + +#[get("/filter")] +async fn get_filtered_config( + req: HttpRequest, + db_conn: DbConnection, +) -> actix_web::Result { + let DbConnection(mut conn) = db_conn; + let params = Query::>::from_query(req.query_string()) + .map_err(|_| ErrorBadRequest("error getting query params"))?; + let mut query_params_map: serde_json::Map = Map::new(); + + for (key, value) in params.0.into_iter() { + query_params_map.insert( + key, + value + .parse::() + .map_or_else(|_| json!(value), |int_val| json!(int_val)), + ); + } + let config = generate_cac(&mut conn).await?; + let contexts = config.contexts; + + let filtered_context = filter_context(contexts, query_params_map)?; + let mut filtered_overrides: Map = Map::new(); + for ele in filtered_context.iter() { + let override_with_key = &ele.override_with_keys[0]; + filtered_overrides.insert( + override_with_key.to_string(), + config + .overrides + .get(override_with_key) + .ok_or(err::InternalServerErr( + "Could not fetch override_key".to_string(), + ))? + .to_owned(), + ); + } + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs, + }; + + Ok(HttpResponse::Ok().json(filtered_config)) +} + +fn filter_context( + contexts: Vec, + query_params_map: Map, +) -> app::Result> { + let mut filtered_context: Vec = Vec::new(); + for context in contexts.into_iter() { + if should_add_ctx(&context, &query_params_map)? { + filtered_context.push(context); + } + } + return Ok(filtered_context); +} + +fn should_add_ctx( + context: &Context, + query_params_map: &Map, +) -> app::Result { + let dimension = extract_dimensions(&context.condition)?; + Ok(dimension + .iter() + .all(|(key, value)| query_params_map.get(key).map_or(true, |val| val == value))) +} diff --git a/crates/context-aware-config/src/api/config/types.rs b/crates/context-aware-config/src/api/config/types.rs index 98b571871..332bfc03f 100644 --- a/crates/context-aware-config/src/api/config/types.rs +++ b/crates/context-aware-config/src/api/config/types.rs @@ -8,7 +8,7 @@ pub struct Config { pub default_configs: Map, } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct Context { pub id: String, pub condition: Value, diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 3b1e3714b..fd6fecd70 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -44,7 +44,7 @@ pub fn validate_override_keys(override_keys: &Vec) -> app::Result<()> { if !key_set.insert(key) { return Err(err::BadArgument(ErrorResponse { message: "override_keys are not unique".to_string(), - possible_fix: "remove duplicate entries in override_keys".to_string() + possible_fix: "remove duplicate entries in override_keys".to_string(), })); } } @@ -52,6 +52,42 @@ pub fn validate_override_keys(override_keys: &Vec) -> app::Result<()> { Ok(()) } +pub fn get_variable_name_and_value(operands: &Vec) -> app::Result<(&str, &Value)> { + let (obj_pos, variable_obj) = operands + .iter() + .enumerate() + .find(|(_, operand)| { + operand.is_object() && operand.as_object().unwrap().get("var").is_some() + }) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable name from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + let variable_name = variable_obj + .as_object() + .map_or(None, |obj| obj.get("var")) + .map_or(None, |value| value.as_str()) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable name from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + let value_pos = (obj_pos + 1) % 2; + let variable_value = + operands + .get(value_pos) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable value from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + Ok((variable_name, variable_value)) +} + pub fn extract_dimensions(context_json: &Value) -> app::Result> { // Assuming max 2-level nesting in context json logic let context = context_json @@ -89,46 +125,7 @@ pub fn extract_dimensions(context_json: &Value) -> app::Result, - override_keys: &Vec + override_keys: &Vec, ) -> bool { if variant_override.keys().len() != override_keys.len() { return false; @@ -201,7 +197,7 @@ pub fn is_valid_experiment( context: &Value, override_keys: &Vec, flags: &ExperimentationFlags, - active_experiments: &Vec + active_experiments: &Vec, ) -> app::Result<(bool, String)> { let mut valid_experiment = true; let mut invalid_reason = String::new(); @@ -319,4 +315,4 @@ pub fn add_variant_dimension_to_ctx( possible_fix: "Check the request sent for correctness".to_string(), })), } -} \ No newline at end of file +} diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs index a17e3223a..00077b027 100644 --- a/crates/experimentation-platform/tests/experimentation_tests.rs +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -20,8 +20,8 @@ fn single_dimension_ctx_gen(value: Dimensions) -> serde_json::Value { }), Dimensions::CLIENT(client_id) => serde_json::json!({ "==": [ - {"var": "clientId"}, - client_id + client_id, + {"var": "clientId"} ] }), } From ac5fe3c579a452a8e7dd753092649e4232d65148 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" Date: Wed, 8 Nov 2023 19:00:04 +0530 Subject: [PATCH 176/352] fix: Removing acceptance of override_keys in experiment create/update --- .../src/api/experiments/handlers.rs | 38 +++++++++++++------ .../src/api/experiments/helpers.rs | 6 +++ .../src/api/experiments/types.rs | 2 - 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 9c15362b0..caa71c78a 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -21,7 +21,8 @@ use service_utils::{ use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, - check_variants_override_coverage, validate_experiment, validate_override_keys, + check_variants_override_coverage, extract_override_keys, validate_experiment, + validate_override_keys, }, types::{ AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, @@ -61,7 +62,12 @@ async fn create( let mut variants = req.variants.to_vec(); let DbConnection(mut conn) = db_conn; - let override_keys = &req.override_keys; + + // Checking if experiment has exactly 1 control variant, and + // atleast 1 experimental variant + check_variant_types(&variants)?; + let unique_override_keys: Vec = + extract_override_keys(&variants[0].overrides).into_iter().collect(); let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); @@ -73,10 +79,7 @@ async fn create( })); } - // Checking if experiment has exactly 1 control variant, and - // atleast 1 experimental variant - validate_override_keys(&override_keys)?; - check_variant_types(&variants)?; + validate_override_keys(&unique_override_keys)?; // Checking if all the variants are overriding the mentioned keys let variant_overrides = variants @@ -84,12 +87,12 @@ async fn create( .map(|variant| &variant.overrides) .collect::>>(); let are_valid_variants = - check_variants_override_coverage(&variant_overrides, override_keys); + check_variants_override_coverage(&variant_overrides, &unique_override_keys); if !are_valid_variants { return Err(err::BadRequest(ErrorResponse { message: "all variants should contain the keys mentioned in override_keys" .to_string(), - possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", override_keys.join(",")) + possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", unique_override_keys.join(",")) })); } @@ -103,8 +106,13 @@ async fn create( // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; - let (valid, reason) = - validate_experiment(&req.context, &req.override_keys, None, &flags, &mut conn)?; + let (valid, reason) = validate_experiment( + &req.context, + &unique_override_keys, + None, + &flags, + &mut conn, + )?; if !valid { return Err(err::BadRequest(ErrorResponse { message: reason, @@ -185,7 +193,7 @@ async fn create( created_at: Utc::now(), last_modified: Utc::now(), name: req.name.to_string(), - override_keys: req.override_keys.to_vec(), + override_keys: unique_override_keys.to_vec(), traffic_percentage: 0, status: ExperimentStatusType::CREATED, context: req.context.clone(), @@ -477,9 +485,15 @@ async fn update_overrides( let experiment_id = params.into_inner(); let payload = req.into_inner(); - let override_keys = payload.override_keys; let variants = payload.variants; + let first_variant = variants.get(0).ok_or(err::BadRequest(ErrorResponse { + message: "Variant not found in request".to_string(), + possible_fix: "Provide at least one entry in variant's list" + .to_string(), + }))?; + let override_keys = extract_override_keys(&first_variant.overrides).into_iter().collect(); + // fetch the current variants of the experiment let experiment = experiments::experiments .find(experiment_id) diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index fd6fecd70..876e15fb6 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -316,3 +316,9 @@ pub fn add_variant_dimension_to_ctx( })), } } + +pub fn extract_override_keys( + overrides: &Map, +) -> HashSet { + overrides.keys().map(String::from).collect() +} diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 27b352a43..0bff31296 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -25,7 +25,6 @@ pub struct Variant { #[derive(Deserialize)] pub struct ExperimentCreateRequest { pub name: String, - pub override_keys: Vec, pub context: Value, pub variants: Vec, @@ -161,7 +160,6 @@ pub struct VariantUpdateRequest { #[derive(Deserialize, Debug)] pub struct OverrideKeysUpdateRequest { - pub override_keys: Vec, pub variants: Vec, } From 00137db5c3501349539aed39b84c3ff92c5b7162 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Thu, 9 Nov 2023 07:55:53 +0000 Subject: [PATCH 177/352] chore(version): v0.14.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421b45bd0..b6a46c8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.14.1 - 2023-11-09 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.7.1 +### Global changes + +- - - + ## v0.14.0 - 2023-11-08 ### Package updates - external bumped to external-v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index 60d01d0e0..6024a06fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index de7212a3c..02b6c9e15 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.7.1 - 2023-11-09 +#### Bug Fixes +- Removing acceptance of override_keys in experiment create/update - (033597e) - ankit.mahato + +- - - + ## experimentation-platform-v0.7.0 - 2023-11-08 #### Features - [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 1488beeed..c67ebf641 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.7.0" +version = "0.7.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 345b4af9c0a5a600232f3408c8d4a28b2a0288e7 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Thu, 5 Oct 2023 12:26:09 +0530 Subject: [PATCH 178/352] feat: added frontend crate,combined frontend and backend binaries () --- .env.example | 2 +- Cargo.lock | 1151 ++++++++++++++++- Cargo.toml | 13 +- crates/context-aware-config/Cargo.toml | 10 +- crates/context-aware-config/src/lib.rs | 5 + crates/context-aware-config/src/main.rs | 37 +- crates/frontend/.gitignore | 13 + crates/frontend/Cargo.toml | 41 + crates/frontend/assets/favicon.ico | Bin 0 -> 15406 bytes crates/frontend/end2end/package-lock.json | 74 ++ crates/frontend/end2end/package.json | 13 + crates/frontend/end2end/playwright.config.ts | 107 ++ crates/frontend/end2end/tests/example.spec.ts | 9 + crates/frontend/src/app.rs | 248 ++++ crates/frontend/src/lib.rs | 20 + crates/frontend/style/tailwind.css | 4 + crates/frontend/tailwind.config.js | 10 + flake.lock | 91 +- flake.nix | 15 +- makefile | 4 +- package-lock.json | 457 ++++--- 21 files changed, 2085 insertions(+), 239 deletions(-) create mode 100644 crates/context-aware-config/src/lib.rs create mode 100644 crates/frontend/.gitignore create mode 100644 crates/frontend/Cargo.toml create mode 100644 crates/frontend/assets/favicon.ico create mode 100644 crates/frontend/end2end/package-lock.json create mode 100644 crates/frontend/end2end/package.json create mode 100644 crates/frontend/end2end/playwright.config.ts create mode 100644 crates/frontend/end2end/tests/example.spec.ts create mode 100644 crates/frontend/src/app.rs create mode 100644 crates/frontend/src/lib.rs create mode 100644 crates/frontend/style/tailwind.css create mode 100644 crates/frontend/tailwind.config.js diff --git a/.env.example b/.env.example index ccf72c0bb..33af043c9 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,6 @@ DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=false TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" diff --git a/Cargo.lock b/Cargo.lock index 6024a06fc..eb0120a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,29 @@ dependencies = [ "smallvec 1.10.0", ] +[[package]] +name = "actix-files" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags 1.3.2", + "bytes 1.4.0", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding 2.2.0", + "pin-project-lite", +] + [[package]] name = "actix-http" version = "3.3.1" @@ -378,6 +401,23 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -386,7 +426,35 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", +] + +[[package]] +name = "attribute-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c124f12ade4e670107b132722d0ad1a5c9790bcbc1b265336369ea05626b4498" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b217a07446e0fb086f83401a98297e2d81492122f5874db5391bd270a185f88" +dependencies = [ + "collection_literals", + "interpolator", + "proc-macro-error", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.32", ] [[package]] @@ -591,6 +659,48 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cached" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700" +dependencies = [ + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures 0.3.28", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", + "tokio 1.29.1", +] + +[[package]] +name = "cached_proc_macro" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082" +dependencies = [ + "cached_proc_macro_types", + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + [[package]] name = "cc" version = "1.0.79" @@ -628,6 +738,33 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.3.4" @@ -661,7 +798,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -689,12 +826,73 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "common_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" + +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "const_format" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.2.5" @@ -707,6 +905,7 @@ version = "0.11.0" dependencies = [ "actix", "actix-cors", + "actix-files", "actix-http", "actix-web", "anyhow", @@ -723,10 +922,15 @@ dependencies = [ "env_logger 0.8.4", "experimentation-platform", "external", + "frontend", "futures 0.3.28", "futures-util", "json-patch", "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", "log", "rand 0.8.5", "reqwest 0.9.24", @@ -754,6 +958,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.12.0" @@ -947,7 +1160,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -964,7 +1177,42 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", ] [[package]] @@ -984,13 +1232,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "derive-where" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146398d62142a0f35248a608f17edf0dde57338354966d6e41d0eb2d16980ccb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "derive_more" version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.0", @@ -1035,7 +1294,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1044,7 +1303,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1088,18 +1347,42 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + [[package]] name = "dtoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.9.0" @@ -1115,6 +1398,19 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -1141,6 +1437,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "erased-serde" version = "0.3.30" @@ -1314,6 +1616,27 @@ dependencies = [ "num", ] +[[package]] +name = "frontend" +version = "0.1.0" +dependencies = [ + "actix-files", + "actix-web", + "cfg-if 1.0.0", + "console_error_panic_hook", + "futures 0.3.28", + "http 0.2.9", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "reqwest 0.11.20", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1408,7 +1731,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1470,6 +1793,39 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.1.26" @@ -1481,7 +1837,7 @@ dependencies = [ "fnv", "futures 0.1.31", "http 0.1.21", - "indexmap", + "indexmap 1.9.3", "log", "slab", "string", @@ -1500,18 +1856,42 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.9", - "indexmap", + "indexmap 1.9.3", "slab", "tokio 1.29.1", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "heck" @@ -1559,6 +1939,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "0.1.21" @@ -1604,6 +1993,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.8.0" @@ -1743,6 +2138,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.1.5" @@ -1782,7 +2183,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.1", ] [[package]] @@ -1794,6 +2205,18 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -1841,6 +2264,15 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -1864,9 +2296,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1883,6 +2315,17 @@ dependencies = [ "treediff", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonlogic" version = "0.5.1" @@ -1944,12 +2387,242 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leptos" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65154cd0fc2f505a1676b870d5c055dec9dafe4d6081358ef1d7e357d6f222c5" +dependencies = [ + "cfg-if 1.0.0", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", +] + +[[package]] +name = "leptos_actix" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a940095989acffbd08f0264e4c7c529a388adbada7dadf26372148c66f221995" +dependencies = [ + "actix-http", + "actix-web", + "futures 0.3.28", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "leptos_router", + "parking_lot 0.12.1", + "regex", + "serde_json", + "tracing", +] + +[[package]] +name = "leptos_config" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0108f6c8409c99fcf25f4c55a56b4bf9afeeb58f643879bb115d4258b9e22979" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a92b7a30d6e1363233211babdd59fdd983f28dc3aa6aebbd7bfbdd15630c73" +dependencies = [ + "async-recursion", + "cfg-if 1.0.0", + "drain_filter_polyfill", + "educe", + "futures 0.3.28", + "getrandom", + "html-escape", + "indexmap 2.0.2", + "itertools", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec 1.10.0", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef84aede40b027d1a4addd9bd54c89de722272429f7b21da40b04f9ebe5e3b2" +dependencies = [ + "anyhow", + "camino", + "indexmap 2.0.2", + "parking_lot 0.12.1", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.32", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdd4411a987054b1bce1f89c888ea1bfde50f9c956ce7edc0bb5b54deaf1621" +dependencies = [ + "futures 0.3.28", + "leptos", + "leptos_config", + "leptos_hot_reload", + "leptos_meta", + "tracing", +] + +[[package]] +name = "leptos_macro" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc27567e059d8ab630a33bf782a81bb2e10178011b8c97c080aafcf09c4e5e0" +dependencies = [ + "attribute-derive", + "cfg-if 1.0.0", + "convert_case 0.6.0", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.32", + "tracing", + "uuid 1.3.4", +] + +[[package]] +name = "leptos_meta" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329c4bbe4191a0bef6514bab827f2ff1a1e69bc2b431e78ac9799e2bdc26ae33" +dependencies = [ + "cfg-if 1.0.0", + "indexmap 2.0.2", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_reactive" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b4fc821e6a8646635b721dd58b5604b5c447eb3b21c464b3837cd2063a6b209" +dependencies = [ + "base64 0.21.2", + "cfg-if 1.0.0", + "futures 0.3.28", + "indexmap 2.0.2", + "js-sys", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen", + "serde_json", + "slotmap", + "thiserror", + "tokio 1.29.1", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f02384aaeff09ba17093305a0dfe8713fb171f7227a8543992a9ce44c75cd" +dependencies = [ + "cached", + "cfg-if 1.0.0", + "common_macros", + "gloo-net", + "js-sys", + "lazy_static", + "leptos", + "linear-map", + "log", + "lru", + "once_cell", + "percent-encoding 2.2.0", + "regex", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tracing", + "url 2.3.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_server" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc28e6ae7ca7bd36fc865fb844ecb27ddf72a0eb9514b7ee45d0cad6cf930c7d" +dependencies = [ + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", +] + [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -1959,6 +2632,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2008,8 +2687,17 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ - "serde", - "value-bag", + "serde", + "value-bag", +] + +[[package]] +name = "lru" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +dependencies = [ + "hashbrown 0.13.2", ] [[package]] @@ -2327,7 +3015,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -2348,12 +3036,28 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + [[package]] name = "parking_lot" version = "0.9.0" @@ -2409,6 +3113,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "1.0.1" @@ -2421,6 +3131,51 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "pest_meta" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.6", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -2438,7 +3193,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -2474,6 +3229,51 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.32", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec 1.10.0", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -2483,6 +3283,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", + "version_check", + "yansi", +] + [[package]] name = "publicsuffix" version = "1.5.6" @@ -2502,6 +3315,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn 2.0.32", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "r2d2" version = "0.8.10" @@ -2815,12 +3651,37 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rs-snowflake" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.32", + "syn_derive", + "thiserror", +] + [[package]] name = "rusoto_core" version = "0.48.0" @@ -2900,16 +3761,32 @@ dependencies = [ "rusoto_credential", "rustc_version 0.4.0", "serde", - "sha2", + "sha2 0.9.9", "tokio 1.29.1", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -2967,6 +3844,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.21" @@ -3030,6 +3916,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + [[package]] name = "semver" version = "0.9.0" @@ -3060,6 +3952,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.180" @@ -3068,7 +3971,7 @@ checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3091,6 +3994,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding 2.2.0", + "serde", + "thiserror", +] + +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.5.5" @@ -3115,6 +4038,56 @@ dependencies = [ "serde", ] +[[package]] +name = "server_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcddd58a35e4fd00f15dac8f2fc08deed175d8178b2c3e615f59a7e7be6fed7" +dependencies = [ + "ciborium", + "const_format", + "gloo-net", + "inventory", + "js-sys", + "lazy_static", + "once_cell", + "proc-macro2", + "quote", + "reqwest 0.11.20", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "syn 2.0.32", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9083155d5a075eda2d08f18663e4789e0d447a1000b225bc4e1746e849c95c8e" +dependencies = [ + "const_format", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "syn 2.0.32", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dba6c99de6539ec3193130f764427ead9d784a76ca3126f38e56a6a0b7a2f3d" +dependencies = [ + "server_fn_macro", + "syn 2.0.32", +] + [[package]] name = "service-utils" version = "0.8.0" @@ -3167,6 +4140,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.6", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -3200,6 +4184,16 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "0.6.14" @@ -3368,15 +4362,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6eef0000c4a12ecdfd7873ea84a8b5aab5e44db72e38e07b028a25386f29a5" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -3429,7 +4435,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3584,7 +4590,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3697,6 +4703,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -3736,7 +4751,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3815,12 +4830,29 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "typed-builder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cba322cb9b7bc6ca048de49e83918223f35e7a86311267013afff257004870" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicase" version = "2.6.0" @@ -3851,6 +4883,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" @@ -3897,6 +4935,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3990,6 +5034,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.2.0" @@ -4025,9 +5079,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -4035,16 +5089,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", "wasm-bindgen-shared", ] @@ -4062,9 +5116,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4072,28 +5126,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4352,6 +5406,27 @@ version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" +[[package]] +name = "xxhash-rust" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 8d23441ea..9f33eb347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,20 @@ [workspace] +resolver = "2" members = [ "crates/service-utils", "crates/context-aware-config", "crates/superposition_client", "crates/cac_client", - "crates/superposition_client_integration_example" + "crates/superposition_client_integration_example", + "crates/frontend" ] + + +[[workspace.metadata.leptos]] +output-name = "frontend" +bin-package = "context-aware-config" +lib-package = "frontend" +assets-dir = "crates/frontend/assets" +style-file = "frontend/style/tailwind.css" +site-root = "crates/frontend" \ No newline at end of file diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index c64502370..3fb5bc407 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -5,14 +5,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [dependencies] cac_client = { path = "../cac_client" } +frontend = {path ="../frontend" } # env dotenv = "0.15.0" # Https server framework actix = "0.13.0" -actix-web = "4.0.0" +actix-web = { version = "4.0.0", features = ["macros"] } # To help generate snowflake ids rs-snowflake = "0.6.0" # To help with generating uuids @@ -57,3 +59,9 @@ external = { path = "../external"} actix-cors = "0.6.4" anyhow = { version = "1.0", default-features = false } dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} +leptos_actix = { version = "0.4.10" } +leptos = { version = "0.4" } +leptos_meta = { version = "0.4" } +leptos_router = { version = "0.4" } +actix-files = { version = "0.6" } + diff --git a/crates/context-aware-config/src/lib.rs b/crates/context-aware-config/src/lib.rs new file mode 100644 index 000000000..a86b33b08 --- /dev/null +++ b/crates/context-aware-config/src/lib.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod db; +pub mod middlewares; +pub mod helpers; + diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 080db3d5a..5cc16ddbc 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -23,16 +23,31 @@ use snowflake::SnowflakeIdGenerator; use std::{sync::Mutex, time::Duration}; use tracing_actix_web::TracingLogger; +use actix_files::Files; +use frontend::app::*; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; use service_utils::{ db::pgschema_manager::PgSchemaManager, db::utils::init_pool_manager, - helpers::{get_from_env_unsafe, get_pod_info, get_from_env_or_default}, + helpers::{get_from_env_or_default, get_from_env_unsafe, get_pod_info}, middlewares::{ app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, }, service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, }; +#[actix_web::get("favicon.ico")] +async fn favicon( + leptos_options: actix_web::web::Data, +) -> actix_web::Result { + let leptos_options = leptos_options.into_inner(); + let site_root = &leptos_options.site_root; + Ok(actix_files::NamedFile::open(format!( + "{site_root}/favicon.ico" + ))?) +} + #[actix_web::main] async fn main() -> Result<()> { dotenv::dotenv().ok(); @@ -95,7 +110,14 @@ async fn main() -> Result<()> { /****** EXPERIMENTATION PLATFORM ENVs *********/ + /* Frontend configurations */ + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); + // Generate the list of routes in your Leptos App + let routes = generate_route_list(|cx| view! { cx, }); + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; App::new() .wrap(DashboardAuth::default(authenticated_routes())) .wrap(TenantMiddlewareFactory) @@ -171,6 +193,19 @@ async fn main() -> Result<()> { AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), ), ) + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", site_root)) + // serve the favicon from /favicon.ico + .service(favicon) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + |cx| view! { cx, }, + ) + .app_data(Data::new(leptos_options.to_owned())) }) .bind(("0.0.0.0", 8080))? .workers(5) diff --git a/crates/frontend/.gitignore b/crates/frontend/.gitignore new file mode 100644 index 000000000..8cdaa33de --- /dev/null +++ b/crates/frontend/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg + +# These are backup files generated by rustfmt +**/*.rs.bk + +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml new file mode 100644 index 000000000..183745461 --- /dev/null +++ b/crates/frontend/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +actix-files = { version = "0.6", optional = true } +actix-web = { version = "4", optional = true, features = ["macros"] } +console_error_panic_hook = "0.1" +cfg-if = "1" +http = { version = "0.2", optional = true } +leptos = { version = "0.4" } +leptos_meta = { version = "0.4" } +leptos_actix = { version = "0.4", optional = true } +leptos_router = { version = "0.4" } +wasm-bindgen = "=0.2.87" +reqwest = { version = "0.11", features = ["json"] } +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +web-sys = "0.3.64" +futures = "0.3" + + +[features] +csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:actix-files", + "dep:actix-web", + "dep:leptos_actix", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + + + + diff --git a/crates/frontend/assets/favicon.ico b/crates/frontend/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 zcmeHOd3aPs5`TblWD*3D%tXPJ#q(n!z$P=3gCjvf#a)E}a;Uf>h{pmVih!a-5LVO` zB?JrzEFicD0wRLo0iPfO372xnkvkzFlRHB)lcTnNZ}KK@US{UKN#b8?e_zkLy1RZ= zT~*y(-6IICgf>E_P6A)M3(wvl2qr-gx_5Ux-_uzT*6_Q&ee1v9B?vzS3&K5IhO2N5 z$9ukLN<`G>$$|GLnga~y%>f}*j%+w@(ixVUb^1_Gjoc;(?TrD3m2)RduFblVN)uy; zQAEd^T{5>-YYH%|Kv{V^cxHMBr1Ik7Frht$imC`rqx@5*| z+OqN!xAjqmaU=qR$uGDMa7p!W9oZ+64($4xDk^FyFQ<_9Z`(;DLnB<;LLJD1<&vnZ zo0(>zIkQTse}qNMb6+i`th54(3pKm8;UAJ<_BULR*Z=m5FU7jiW(&#l+}WkHZ|e@1 z`pm;Q^pCuLUQUrnQ(hPM10pSSHQS=Bf8DqG1&!-B!oQQ|FuzLruL1w(+g<8&znyI? zzX-}?SwUvNjEuT?7uUOy{Fb@xKklpj+jdYM^IK9}NxvLRZd{l9FHEQJ4IO~q%4I0O zAN|*8x^nIU4Giw?f*tmNx=7H)2-Zn?J^B6SgpcW3ZXV_57Sn%Mtfr_=w|sYpAhdJT zcKo6Z*oIOU(az~3$LOEWm9Q)dYWMA}T7L23MVGqrcA%4H)+^`+=j+Hh8CTCnnG2Rh zgcXVW%F8$R9)6}f=NQiLPt8qt3xNUQI>Q*)H1lzk<&n?XR-f}tc&9V0H0lhGqHJ^N zN%h(9-Of2_)!Xk{qdIkU>1%mk%I_Id1!MU*yq&&>)Q+!L^t&-2mW9Xq7g9C@* zl&PKJ&su2L+iku?Te?Pf?k3tUK){Bj_gb&aPo8Ago^XI~mRTd(5{&^tf1)!-lSMha z@$~ae!r(~`=p&|mMxy2EiZQ6FvXb(1avS*`Pj%$)*?vwceGKHmHnl`v&fEQ_Wh+G) zEPQ^3&oV%}%;zF`AM|S%d>pM@1}33PN5*4SewROk_K$n^i8QjaYiRzwG8#OvVIF|{x85wH+?*P*%)woI zR538k@=(E`V;p1UwA|fqSh`$n_t;Sz4T)`_s~pRR4lbmWWSdxa-FqLZ%fLT)Bh?iye?COx~mO1wkn5)HNMg7`8~ z25VJhz&3Z7`M>6luJrEw$Jikft+6SxyIh?)PU1?DfrKMGC z=3T;;omE4H`PWqF8?0*dOA3o9y@~WK`S}{?tIHquEw?v`M^D%Lobpdrp%3}1=-&qk zqAtb1px-1Fy6}E8IUg4s%8B0~P<P5C;de%@n~XnDKF@fr$a+^@$^P|>vlw($aSK2lRtLt~8tRb`I0 znfI!G?K|<5ry*gk>y56rZy0NkK6)))6Mg1=K?7yS9p+#1Ij=W*%5Rt-mlc;#MOnE9 zoi`-+6oj@)`gq2Af!B+9%J#K9V=ji2dj2<_qaLSXOCeqQ&<0zMSb$5mAi;HU=v`v<>NYk}MbD!ewYVB+N-ctzn=l&bTwv)*7 zmY<+Y@SBbtl9PPk$HTR?ln@(T92XjTRj0Mx|Mzl;lW>Su_y^~fh?8(L?oz8h!cCpb zZG-OY=NJ3{>r*`U<(J%#zjFT-a9>u6+23H{=d(utkgqt7@^)C;pkb)fQ|Q=*8*SyT z;otKe+f8fEp)ZacKZDn3TNzs>_Kx+g*c_mr8LBhr8GnoEmAQk#%sR52`bdbW8Ms$!0u2bdt=T-lK3JbDW`F(Urt%Ob2seiN>7U`YN}aOdIiCC;eeufJC#m3S z9#|l2c?G@t*hH5y^76jkv)rs4H+;oiTuY5FQwRMN_7NUqeiD|b&RyxPXQz|3qC(_> zZJMwjC4F!1m2INXqzisQ4X^w=>&(+Ecdu&~IWEMn7f*YcYI&eWI(6hI#f114%aymM zyhlG6{q>XN7(LyGiMAS&qijR%d2rV|>AUT_sE&EKUSTCM26>aKzNxk0?K|utOcxl# zxIOwM#O!!H+QzbX*&p=QuKe4y;bS>&StQOE5AEGg_ubk8{;1yOVAJfE_Js-lL7rr9 z)CEuFIlkApj~uV^zJK7KocjT=4B zJP(}0x}|A7C$$5gIp>KBPZ|A#2Ew;$#g9Fk)r;Q~?G$>x<+JM)J3u>j zi68K=I;ld`JJ?Nq+^_B?C+Q%+x#m{9JF$tbaDeNIep%=^#>KHGtg=L)>m z_J&vaZTs2{qP!4Gdw5u5Kcf}5R4(q}Lebx%(J$7l*Q`Il#pCTM%!`y5y*-~zIVs}D z9;t+(xmV~R65^ZQXe+<5{$QW0O8MT~a{kdFLR)nfRMA9L(YU>x*DTltN#m-2km zC;T`cfb{c`mcx(z7o_a8bYJn8_^dz4Cq!DZ37{P6uF{@#519UWK1{>(9sZB1I^6MmNc39MJ-_|)!S8vO+O3&$MulU3Gc z_W{N*B(yneyl-oN_MKaJ{CZ6dv-~^8uPbLSh&0jfV@EfA{2Dc!_rOyfx`R0T@LonA z<*%O?-aa_Wm-z$s@K(ex7UhM0-?9C=PkYdk&d2n((E4>&(f4D`fOQY%CURMMyJyU` zVeJBAId&StHjw76tnwSqZs3e0683`L{a3k9JYdg#(ZVw4J`&CkV-2LFaDE1Z?CehVy%vZx$tM3tTax8E@2;N^QTrPcI?Ob8uK!DM0_sfE6ks2M?iw zPS4{(k-PF*-oY>S!d9;L+|xdTtLen9B2LvpL4k;#ScB< z$NP_7j~7)5eXuoYEk*dK_rSz9yT_C4B{r~^#^o}-VQI=Y?01|$aa!a7=UEm$|DsQQ zfLK1qmho2@)nwA?$1%T6jwO2HZ({6&;`s|OQOxI4S8*Hw=Qp!b(gNJR%SAj&wGa>^&2@x)Vj zhd^WfzJ^b0O{E^q82Pw({uT`E`MT2WnZ02{E%t*yRPN>?W>0vU^4@Vyh4;mLj918c z*s*papo?<}cQM{5lcgZScx}?usg{mS!KkH9U%@|^_33?{FI{1ss+8kXyFY&5M-e~f zM$){FF;_+z3sNJ)Er~{Beux$fEl{R4|7WKcpEsGtK57f+H0DJ$hI;U;JtF>+lG@sV zQI_;bQ^7XIJ>Bs?C32b1v;am;P4GUqAJ#zOHv}4SmV|xXX6~O9&e_~YCCpbT>s$`! k<4FtN!5=14" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + } + }, + "dependencies": { + "@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.0" + } + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true + } + } +} diff --git a/crates/frontend/end2end/package.json b/crates/frontend/end2end/package.json new file mode 100644 index 000000000..ed785859f --- /dev/null +++ b/crates/frontend/end2end/package.json @@ -0,0 +1,13 @@ +{ + "name": "end2end", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0" + } +} diff --git a/crates/frontend/end2end/playwright.config.ts b/crates/frontend/end2end/playwright.config.ts new file mode 100644 index 000000000..e9891c094 --- /dev/null +++ b/crates/frontend/end2end/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./tests", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/crates/frontend/end2end/tests/example.spec.ts b/crates/frontend/end2end/tests/example.spec.ts new file mode 100644 index 000000000..a461f351a --- /dev/null +++ b/crates/frontend/end2end/tests/example.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from "@playwright/test"; + +test("homepage has title and links to intro page", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + await expect(page).toHaveTitle("Welcome to Leptos"); + + await expect(page.locator("h1")).toHaveText("Welcome to Leptos!"); +}); diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs new file mode 100644 index 000000000..8a001d259 --- /dev/null +++ b/crates/frontend/src/app.rs @@ -0,0 +1,248 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec, + pub overrides: Map, + pub default_configs: Map, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} + +pub async fn fetch_config(tenant: String) -> Result { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + let url = format!("{host}/config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +#[component] +pub fn App(cx: Scope) -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(cx); + view! { cx, + // injects a stylesheet into the document + // id=leptos means cargo-leptos will hot-reload this stylesheet + + + + // sets the document title + + // content for this welcome page + <Router> + <main> + <Routes> + <Route ssr=SsrMode::PartiallyBlocked path="" view=HomePage/> + <Route path="/*any" view=NotFound/> + </Routes> + </main> + </Router> + } +} + +#[component] +fn HomePage(cx: Scope) -> impl IntoView { + let query = use_query_map(cx); + + let tenant = + query.with(|params_map| params_map.get("tenant").cloned().unwrap_or_default()); + let config_data = + create_blocking_resource(cx, || {}, move |_| fetch_config(tenant.clone())); + + view! { cx, + <div class="container mt-5" > + <div class="text-center mb-4"> + <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> + </div> + <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> + { + config_data.with(cx, move |result| { + match result { + Ok(config) => { + let rows = |k:&String, v:&Value| { + let key = k.replace("\"", "").trim().to_string(); + let value = format!("{}", v).replace("\"", "").trim().to_string(); + view! { cx, + <tr> + <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> + <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> + </tr> + } + }; + + let contexts_views: Vec<_> = config.contexts.iter().map(|context| { + let condition = extract_and_format(&context.condition); + let rows: Vec<_> = context.override_with_keys.iter() + .filter_map(|key| config.overrides.get(key)) + .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .map(|(k, v)| { + rows(&k,&v) + }).collect(); + + view! { cx, + <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> + <table class="table table-responsive table-bordered table-hover border-secondary"> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody class="bg-light"> + { rows } + </tbody> + </table> + } + }).collect::<Vec<_>>(); + + let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); + let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ + rows(&k,&v) + }).collect(); + + vec![ + view! { cx, + <div class="mb-4 "> + { new_context_views } + <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> + <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody> + {default_config} + </tbody> + </table> + </div> + } + ] + }, + Err(error) => { + vec![ + view! { cx, + <div class="error"> + {"Failed to fetch config data: "} + {error} + </div> + } + ] + } + } + }) + } + </Suspense> + </div> + + + } +} +/// 404 - Not Found +#[component] +fn NotFound(cx: Scope) -> impl IntoView { + // set an HTTP status code 404 + // this is feature gated because it can only be done during + // initial server-side rendering + // if you navigate to the 404 page subsequently, the status + // code will not be set because there is not a new HTTP request + // to the server + #[cfg(feature = "ssr")] + { + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + let resp = expect_context::<leptos_actix::ResponseOptions>(cx); + resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + } + view! { cx, + <h1>"Not Found"</h1> + } +} + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs new file mode 100644 index 000000000..ec6114fe2 --- /dev/null +++ b/crates/frontend/src/lib.rs @@ -0,0 +1,20 @@ +pub mod app; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "hydrate")] { + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + pub fn hydrate() { + use app::*; + use leptos::*; + + console_error_panic_hook::set_once(); + + leptos::mount_to_body(move |cx| { + view! { cx, <App/> } + }); + } + } +} diff --git a/crates/frontend/style/tailwind.css b/crates/frontend/style/tailwind.css new file mode 100644 index 000000000..510ff1d53 --- /dev/null +++ b/crates/frontend/style/tailwind.css @@ -0,0 +1,4 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@tailwind variants; diff --git a/crates/frontend/tailwind.config.js b/crates/frontend/tailwind.config.js new file mode 100644 index 000000000..52c58ebfc --- /dev/null +++ b/crates/frontend/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs"], + }, + theme: { + extend: {}, + }, + plugins: [], + } \ No newline at end of file diff --git a/flake.lock b/flake.lock index 26c06dd40..c9436efe3 100644 --- a/flake.lock +++ b/flake.lock @@ -4,6 +4,24 @@ "inputs": { "systems": "systems" }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, "locked": { "lastModified": 1681202837, "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", @@ -23,11 +41,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1679567394, - "narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=", + "lastModified": 1694081375, + "narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=", "owner": "nix-community", "repo": "naersk", - "rev": "88cd22380154a2c36799fe8098888f0f59861a15", + "rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89", "type": "github" }, "original": { @@ -38,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1683657389, - "narHash": "sha256-jx91UqqoBneE8QPAKJA29GANrU/Z7ULghoa/JE0+Edw=", + "lastModified": 1697379843, + "narHash": "sha256-RcnGuJgC2K/UpTy+d32piEoBXq2M+nVFzM3ah/ZdJzg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9524f57dd5b3944c819dd594aed8ed941932ef56", + "rev": "12bdeb01ff9e2d3917e6a44037ed7df6e6c3df9d", "type": "github" }, "original": { @@ -52,16 +70,32 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1686431482, - "narHash": "sha256-oPVQ/0YP7yC2ztNsxvWLrV+f0NQ2QAwxbrZ+bgGydEM=", + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1681358109, + "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d3bb401dcfc5a46ce51cdfb5762e70cc75d082d2", + "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.05", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -70,7 +104,27 @@ "inputs": { "flake-utils": "flake-utils", "naersk": "naersk", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1697508761, + "narHash": "sha256-QKWiXUlnke+EiJw3pek1l7xyJ4YsxYXZeQJt/YLgjvA=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "6f74c92caaf2541641b50ec623676430101d1fd4", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { @@ -87,6 +141,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 54860de36..76dbaeead 100644 --- a/flake.nix +++ b/flake.nix @@ -2,14 +2,16 @@ inputs = { flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; - nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { url = "github:oxalica/rust-overlay"; }; }; - outputs = { self, flake-utils, naersk, nixpkgs }: + outputs = { self,rust-overlay, flake-utils, naersk, nixpkgs }: flake-utils.lib.eachDefaultSystem (system: let pkgs = (import nixpkgs) { inherit system; + overlays = [ rust-overlay.overlay ]; }; naersk' = pkgs.callPackage naersk {}; @@ -33,7 +35,6 @@ let univPkgs = with pkgs; [ # Build requirements - rustc cargo libiconv openssl @@ -47,13 +48,19 @@ diesel-cli docker-compose stdenv.cc - pkgconfig + pkg-config awscli jq nodejs_18 + wasm-pack + ( rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + targets = [ "wasm32-unknown-unknown" ]; + }) ]; darwinPkgs = with pkgs; [ darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration ]; in univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else []); diff --git a/makefile b/makefile index e9e2a1cb5..c5286938e 100644 --- a/makefile +++ b/makefile @@ -86,10 +86,11 @@ kill: cac: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ - cargo run --color always --bin context-aware-config + cargo run --color always --bin context-aware-config --no-default-features --features=ssr run: -make kill + cd crates/frontend && wasm-pack build --target=web --debug --no-default-features --features=hydrate cargo build --color always while ! make validate-psql-connection validate-aws-connection; \ do echo "waiting for postgres, localstack bootup"; \ @@ -129,5 +130,4 @@ registry-login: --username AWS \ --password-stdin $(REGISTRY_HOST) - default: dev-build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3204ced58..fc172b014 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,22 @@ "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "dev": true + }, "node_modules/@postman/form-data": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", @@ -25,6 +41,21 @@ "node": ">= 6" } }, + "node_modules/@postman/tough-cookie": { + "version": "4.1.3-postman.1", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", + "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@postman/tunnel-agent": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", @@ -102,9 +133,9 @@ } }, "node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, "node_modules/asynckit": { @@ -193,9 +224,9 @@ } }, "node_modules/chardet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.4.0.tgz", - "integrity": "sha512-NpwMDdSIprbYx1CLnfbxEIarI0Z+s9MssEgggMNheGM+WD68yOhV7IEA/3r6tr0yTRgQD0HuZJDw32s99i6L+A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.6.0.tgz", + "integrity": "sha512-+QOTw3otC4+FxdjK9RopGpNOglADbr4WPFi0SonkO99JbpkTPbMxmdm4NenhF5Zs+4gPXLI1+y2uazws5TMe8w==", "dev": true }, "node_modules/charset": { @@ -208,21 +239,21 @@ } }, "node_modules/cli-progress": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.10.0.tgz", - "integrity": "sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", "dev": true, "dependencies": { - "string-width": "^4.2.0" + "string-width": "^4.2.3" }, "engines": { "node": ">=4" } }, "node_modules/cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -231,7 +262,7 @@ "node": "10.* || >= 12.*" }, "optionalDependencies": { - "colors": "1.4.0" + "@colors/colors": "1.5.0" } }, "node_modules/color-convert": { @@ -318,15 +349,6 @@ "node": ">=8" } }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -369,6 +391,16 @@ "node": ">=0.4.0" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/directory-tree": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", @@ -431,12 +463,6 @@ "node >=0.6.0" ] }, - "node_modules/faker": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", - "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", - "dev": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -459,12 +485,12 @@ } }, "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz", + "integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==", "dev": true, "engines": { - "node": ">= 0.4.0" + "node": ">= 10.4.0" } }, "node_modules/find-replace": { @@ -480,9 +506,9 @@ } }, "node_modules/flatted": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", - "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", "dev": true }, "node_modules/forever-agent": { @@ -504,13 +530,13 @@ } }, "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "dependencies": { "minimist": "^1.2.5", - "neo-async": "^2.6.0", + "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, @@ -577,22 +603,34 @@ } }, "node_modules/httpntlm": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.7.7.tgz", - "integrity": "sha512-Pv2Rvrz8H0qv1Dne5mAdZ9JegG1uc6Vu5lwLflIY6s8RKHdZQbW39L4dYswSgqMDT0pkJILUTKjeyU0VPNRZjA==", + "version": "1.8.13", + "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz", + "integrity": "sha512-2F2FDPiWT4rewPzNMg3uPhNkP3NExENlUGADRUDPQvuftuUTGW98nLZtGemCIW3G40VhWZYgkIDcQFAwZ3mf2Q==", "dev": true, + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=2CKNJLZJBW8ZC" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/samdecrock" + } + ], "dependencies": { + "des.js": "^1.0.1", "httpreq": ">=0.4.22", + "js-md4": "^0.3.2", "underscore": "~1.12.1" }, "engines": { - "node": ">=0.8.0" + "node": ">=10.4.0" } }, "node_modules/httpreq": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.0.tgz", - "integrity": "sha512-P2ROAc2JG4Y1+EL8vRusYb3p6BK5WRZLVj8WLrvtQJL9qQgocqieU9+0cWWwrL2FroUjXGtUDo4lOKXoq+Et+g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.1.tgz", + "integrity": "sha512-uhSZLPPD2VXXOSN8Cni3kIsoFHaU2pT/nySEU/fHr/ePbqHYr0jeiQRmUKLEirC09SFPsdMoA7LU7UXMd/w0Kw==", "dev": true, "engines": { "node": ">= 6.15.1" @@ -616,15 +654,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -646,6 +675,21 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true + }, "node_modules/js-sha512": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", @@ -725,9 +769,9 @@ } }, "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "engines": { "node": ">= 0.6" @@ -743,17 +787,23 @@ } }, "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "dependencies": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -764,15 +814,18 @@ } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/neo-async": { @@ -782,40 +835,49 @@ "dev": true }, "node_modules/newman": { - "version": "5.3.2", - "resolved": "git+ssh://git@github.com/knutties/newman.git#37708c1fbf6b36240ac796a42dc5a7b435990764", + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/knutties/newman.git#7bdb3067b77b39a04520411c6cd89fb998a33534", "dev": true, "license": "Apache-2.0", "dependencies": { - "async": "3.2.3", - "chardet": "1.4.0", - "cli-progress": "3.10.0", - "cli-table3": "0.6.1", + "@postman/tough-cookie": "4.1.3-postman.1", + "async": "3.2.4", + "chardet": "1.6.0", + "cli-progress": "3.12.0", + "cli-table3": "0.6.3", "colors": "1.4.0", - "commander": "7.2.0", + "commander": "11.0.0", "csv-parse": "4.16.3", "directory-tree": "3.5.1", "eventemitter3": "4.0.7", - "filesize": "8.0.7", + "filesize": "10.0.12", "liquid-json": "0.3.1", "lodash": "4.17.21", - "mkdirp": "1.0.4", - "postman-collection": "4.1.1", - "postman-collection-transformer": "4.1.6", - "postman-request": "2.88.1-postman.31", - "postman-runtime": "7.29.0", + "mkdirp": "3.0.1", + "postman-collection": "4.2.1", + "postman-collection-transformer": "4.1.7", + "postman-request": "2.88.1-postman.33", + "postman-runtime": "7.33.0", "pretty-ms": "7.0.1", - "semver": "7.3.5", + "semver": "7.5.4", "serialised-error": "1.1.3", - "tough-cookie": "3.0.1", - "word-wrap": "1.2.3", + "word-wrap": "1.2.5", "xmlbuilder": "15.1.1" }, "bin": { "newman": "bin/newman.js" }, "engines": { - "node": ">=10" + "node": ">=16" + } + }, + "node_modules/newman/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" } }, "node_modules/node-oauth1": { @@ -833,15 +895,6 @@ "node": "*" } }, - "node_modules/object-hash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", - "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -858,21 +911,21 @@ "dev": true }, "node_modules/postman-collection": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.1.1.tgz", - "integrity": "sha512-ODpJtlf8r99DMcTU7gFmi/yvQYckFzcuE6zL/fWnyrFT34ugdCBFlX+DN7M+AnP6lmR822fv5s60H4DnL4+fAg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", + "integrity": "sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==", "dev": true, "dependencies": { - "faker": "5.5.3", + "@faker-js/faker": "5.5.3", "file-type": "3.9.0", "http-reasons": "0.1.0", "iconv-lite": "0.6.3", "liquid-json": "0.3.1", "lodash": "4.17.21", "mime-format": "2.0.1", - "mime-types": "2.1.34", + "mime-types": "2.1.35", "postman-url-encoder": "3.0.5", - "semver": "7.3.5", + "semver": "7.5.4", "uuid": "8.3.2" }, "engines": { @@ -880,15 +933,15 @@ } }, "node_modules/postman-collection-transformer": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/postman-collection-transformer/-/postman-collection-transformer-4.1.6.tgz", - "integrity": "sha512-xvdQb6sZoWcG9xZXUPSuxocjcd6WCZlINlGGiuHdSfxhgiwQhj9qhF0JRFbagZ8xB0+pYUairD5MiCENc6DEVA==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/postman-collection-transformer/-/postman-collection-transformer-4.1.7.tgz", + "integrity": "sha512-SxJkm/LnlFZs2splUBnS4jQFicgBptghpm4voHtNnaum3Ad64E2MHLV4fJhv58dVUmFwdSwdQUN3m2q0iLecnQ==", "dev": true, "dependencies": { "commander": "8.3.0", "inherits": "2.0.4", "lodash": "4.17.21", - "semver": "7.3.5", + "semver": "7.5.4", "strip-json-comments": "3.1.1" }, "bin": { @@ -908,16 +961,17 @@ } }, "node_modules/postman-request": { - "version": "2.88.1-postman.31", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.31.tgz", - "integrity": "sha512-OJbYqP7ItxQ84yHyuNpDywCZB0HYbpHJisMQ9lb1cSL3N5H3Td6a2+3l/a74UMd3u82BiGC5yQyYmdOIETP/nQ==", + "version": "2.88.1-postman.33", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.33.tgz", + "integrity": "sha512-uL9sCML4gPH6Z4hreDWbeinKU0p0Ke261nU7OvII95NU22HN6Dk7T/SaVPaj6T4TsQqGKIFw6/woLZnH7ugFNA==", "dev": true, "dependencies": { "@postman/form-data": "~3.1.1", + "@postman/tough-cookie": "~4.1.3-postman.1", "@postman/tunnel-agent": "^0.6.3", "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "brotli": "~1.3.2", + "aws4": "^1.12.0", + "brotli": "^1.3.3", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", @@ -927,84 +981,101 @@ "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", + "mime-types": "^2.1.35", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "~6.5.3", "safe-buffer": "^5.1.2", "stream-length": "^1.0.2", - "tough-cookie": "~2.5.0", - "uuid": "^3.3.2" + "uuid": "^8.3.2" }, "engines": { "node": ">= 6" } }, - "node_modules/postman-request/node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "node_modules/postman-runtime": { + "version": "7.33.0", + "resolved": "https://registry.npmjs.org/postman-runtime/-/postman-runtime-7.33.0.tgz", + "integrity": "sha512-cYCb+5Y12FwZU/T3gOj2SKiOz38pisVLc0tdppb+ZlG7iqn5aLgxghJwhjG62pZCV6uixKiQX1hNdLSk9a9Xtw==", "dev": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "@postman/tough-cookie": "4.1.3-postman.1", + "async": "3.2.4", + "aws4": "1.12.0", + "handlebars": "4.7.8", + "httpntlm": "1.8.13", + "jose": "4.14.4", + "js-sha512": "0.8.0", + "lodash": "4.17.21", + "mime-types": "2.1.35", + "node-oauth1": "1.3.0", + "performance-now": "2.1.0", + "postman-collection": "4.2.0", + "postman-request": "2.88.1-postman.33", + "postman-sandbox": "4.2.7", + "postman-url-encoder": "3.0.5", + "serialised-error": "1.1.3", + "strip-json-comments": "3.1.1", + "uuid": "8.3.2" }, "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/postman-request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/postman-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/postman-runtime/-/postman-runtime-7.29.0.tgz", - "integrity": "sha512-eXxHREE/fUpohkGPRgBY1YccSGx9cyW3mtGiPyIE4zD5fYzasgBHqW6kbEND3Xrd3yf/uht/YI1H8O7J1+A1+w==", + "node_modules/postman-runtime/node_modules/postman-collection": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.0.tgz", + "integrity": "sha512-tvOLgN1h6Kab6dt43PmBoV5kYO/YUta3x0C2QqfmbzmHZe47VTpZ/+gIkGlbNhjKNPUUub5X6ehxYKoaTYdy1w==", "dev": true, "dependencies": { - "async": "3.2.3", - "aws4": "1.11.0", - "handlebars": "4.7.7", - "httpntlm": "1.7.7", - "js-sha512": "0.8.0", + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", "lodash": "4.17.21", - "mime-types": "2.1.34", - "node-oauth1": "1.3.0", - "performance-now": "2.1.0", - "postman-collection": "4.1.1", - "postman-request": "2.88.1-postman.31", - "postman-sandbox": "4.0.6", + "mime-format": "2.0.1", + "mime-types": "2.1.35", "postman-url-encoder": "3.0.5", - "serialised-error": "1.1.3", - "tough-cookie": "3.0.1", + "semver": "7.5.4", "uuid": "8.3.2" }, "engines": { "node": ">=10" } }, - "node_modules/postman-runtime/node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, "node_modules/postman-sandbox": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postman-sandbox/-/postman-sandbox-4.0.6.tgz", - "integrity": "sha512-PPRanSNEE4zy3kO7CeSBHmAfJnGdD9ecHY/Mjh26CQuZZarGkNO8c0U/n+xX3+5M1BRNc82UYq6YCtdsSDqcng==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/postman-sandbox/-/postman-sandbox-4.2.7.tgz", + "integrity": "sha512-/EcCrKnb/o+9iLS4u+H76E0kBomJFjPptVjoDiq1uZ7Es/4aTv0MAX+0aoDxdDO+0h9sl8vy65uKQwyjN7AOaw==", "dev": true, "dependencies": { "lodash": "4.17.21", + "postman-collection": "4.2.0", "teleport-javascript": "1.0.0", - "uvm": "2.0.2" + "uvm": "2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-sandbox/node_modules/postman-collection": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.0.tgz", + "integrity": "sha512-tvOLgN1h6Kab6dt43PmBoV5kYO/YUta3x0C2QqfmbzmHZe47VTpZ/+gIkGlbNhjKNPUUub5X6ehxYKoaTYdy1w==", + "dev": true, + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.5.4", + "uuid": "8.3.2" }, "engines": { "node": ">=10" @@ -1061,6 +1132,12 @@ "node": ">=0.6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/reduce-flatten": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", @@ -1070,6 +1147,12 @@ "node": ">=6" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1097,9 +1180,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1122,6 +1205,15 @@ "uuid": "^3.0.0" } }, + "node_modules/serialised-error/node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/serialised-error/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -1273,20 +1365,6 @@ "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", "dev": true }, - "node_modules/tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "dependencies": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -1321,6 +1399,15 @@ "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", "dev": true }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1330,6 +1417,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -1340,12 +1437,12 @@ } }, "node_modules/uvm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/uvm/-/uvm-2.0.2.tgz", - "integrity": "sha512-Ra+aPiS5GXAbwXmyNExqdS42sTqmmx4XWEDF8uJlsTfOkKf9Rd9xNgav1Yckv4HfVEZg4iOFODWHFYuJ+9Fzfg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/uvm/-/uvm-2.1.1.tgz", + "integrity": "sha512-BZ5w8adTpNNr+zczOBRpaX/hH8UPKAf7fmCnidrcsqt3bn8KT9bDIfuS7hgRU9RXgiN01su2pwysBONY6w8W5w==", "dev": true, "dependencies": { - "flatted": "3.1.1" + "flatted": "3.2.6" }, "engines": { "node": ">=10" @@ -1366,9 +1463,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" From 3611ef31958b0008bc345481d7697c6aeb5b2ed9 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 12 Oct 2023 17:21:38 +0530 Subject: [PATCH 179/352] feat: added format check in the JenkinsFile() --- Jenkinsfile | 19 ++++++- crates/cac_client/src/eval.rs | 2 +- crates/cac_client/src/lib.rs | 52 ++++++++++--------- .../src/api/audit_log/handlers.rs | 2 +- .../src/api/config/handlers.rs | 5 +- .../src/api/context/types.rs | 2 +- .../src/api/default_config/handlers.rs | 2 +- .../src/api/default_config/mod.rs | 2 +- .../src/api/dimension/handlers.rs | 2 +- .../src/api/dimension/utils.rs | 7 ++- crates/context-aware-config/src/db/models.rs | 2 +- crates/context-aware-config/src/lib.rs | 3 +- .../src/middlewares/audit_response_header.rs | 6 +-- .../src/api/experiments/handlers.rs | 14 ++--- .../src/api/experiments/helpers.rs | 4 +- .../src/api/experiments/types.rs | 4 +- .../experimentation-platform/src/db/schema.rs | 2 +- .../tests/experimentation_tests.rs | 12 +++-- crates/external/src/api/external_api/mod.rs | 2 +- crates/external/src/api/external_api/types.rs | 4 +- crates/external/src/api/mod.rs | 2 +- crates/service-utils/src/aws/kms.rs | 3 +- .../service-utils/src/db/pgschema_manager.rs | 4 +- crates/service-utils/src/db/utils.rs | 4 +- crates/service-utils/src/errors/mod.rs | 2 +- crates/service-utils/src/errors/types.rs | 2 +- crates/service-utils/src/helpers.rs | 10 +++- .../src/middlewares/app_scope.rs | 2 +- crates/service-utils/src/middlewares/mod.rs | 2 +- .../service-utils/src/middlewares/tenant.rs | 23 +++++--- crates/superposition_client/src/lib.rs | 23 ++++---- crates/superposition_client/src/types.rs | 2 +- .../src/main.rs | 2 +- 33 files changed, 133 insertions(+), 96 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1102e61c4..a1f0af955 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -53,7 +53,21 @@ pipeline { stage('Test') { when { expression { SKIP_CI == 'false' } } - steps { sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' } + steps { + script { + def fmtResultCode = sh(script: 'cargo fmt --check', returnStatus:true) + if (fmtResultCode != 0){ + error("Code is not formatted properly. Please run 'cargo fmt' and commit the changes.") + } + + def commitResultCode = sh(script: 'cog check --from-latest-tag', returnStatus:true) + if (commitResultCode != 0) { + error("Commit message does not follow Conventional Commits guidelines.") + } + } + + sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' + } } stage('Get old Version') { @@ -212,4 +226,5 @@ pipeline { } } } -} \ No newline at end of file +} + diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index 512bbe46c..63658f604 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -53,4 +53,4 @@ pub fn eval_cac( merge_overrides_on_default_config(&mut default_config, overrides); let overriden_config = default_config; Ok(overriden_config) -} \ No newline at end of file +} diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index a75d1d475..adc457beb 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -45,20 +45,16 @@ fn clone_reqw(reqw: &RequestBuilder) -> Result<RequestBuilder, String> { .ok_or_else(|| "Unable to clone reqw".to_string()) } -fn get_last_modified( - resp: &Response -) -> Option<DateTime<Utc>> { - resp.headers() - .get("last-modified") - .and_then(|header_val| { - let header_str = header_val.to_str().ok()?; - DateTime::parse_from_rfc2822(header_str) - .map(|datetime| datetime.with_timezone(&Utc)) - .map_err(|e| { - log::error!("Failed to parse date: {e}"); - }) - .ok() - }) +fn get_last_modified(resp: &Response) -> Option<DateTime<Utc>> { + resp.headers().get("last-modified").and_then(|header_val| { + let header_str = header_val.to_str().ok()?; + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc)) + .map_err(|e| { + log::error!("Failed to parse date: {e}"); + }) + .ok() + }) } impl Client { @@ -78,7 +74,7 @@ impl Client { let resp = reqwc.send().await.map_err_to_string()?; let last_modified_at = get_last_modified(&resp); let config = resp.json::<Config>().await.map_err_to_string()?; - + let client = Client { tenant, reqw: Data::new(reqw), @@ -100,9 +96,15 @@ impl Client { let resp = reqw.send().await.map_err_to_string()?; match resp.status() { StatusCode::NOT_MODIFIED => { - return Err(String::from(format!("{} CAC: skipping update, remote not modified", self.tenant))); + return Err(String::from(format!( + "{} CAC: skipping update, remote not modified", + self.tenant + ))); } - StatusCode::OK => log::info!("{}", format!("{} CAC: new config received, updating", self.tenant)), + StatusCode::OK => log::info!( + "{}", + format!("{} CAC: new config received, updating", self.tenant) + ), x => return Err(format!("{} CAC: fetch failed, status: {}", self.tenant, x)), }; Ok(resp) @@ -154,7 +156,7 @@ impl Client { } #[derive(Deref, DerefMut)] -pub struct ClientFactory ( RwLock<HashMap<String, Arc<Client>>> ); +pub struct ClientFactory(RwLock<HashMap<String, Arc<Client>>>); impl ClientFactory { pub async fn create_client( &self, @@ -180,8 +182,9 @@ impl ClientFactory { tenant.to_string(), update_config_periodically, polling_interval, - hostname - ).await? + hostname, + ) + .await?, ); factory.insert(tenant.to_string(), client.clone()); return Ok(client.clone()); @@ -198,14 +201,13 @@ impl ClientFactory { match factory.get(&tenant) { Some(client) => Ok(client.clone()), - None => Err("No such tenant found".to_string()) + None => Err("No such tenant found".to_string()), } } } use once_cell::sync::Lazy; -pub static CLIENT_FACTORY: Lazy<ClientFactory> = Lazy::new(|| { - ClientFactory(RwLock::new(HashMap::new())) -}); +pub static CLIENT_FACTORY: Lazy<ClientFactory> = + Lazy::new(|| ClientFactory(RwLock::new(HashMap::new()))); -pub use eval::eval_cac; \ No newline at end of file +pub use eval::eval_cac; diff --git a/crates/context-aware-config/src/api/audit_log/handlers.rs b/crates/context-aware-config/src/api/audit_log/handlers.rs index 610451b8a..363a51b5b 100644 --- a/crates/context-aware-config/src/api/audit_log/handlers.rs +++ b/crates/context-aware-config/src/api/audit_log/handlers.rs @@ -60,4 +60,4 @@ async fn get_audit_logs( "total_pages": total_pages, "data": logs }))) -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 27cbd836c..9576cd7a6 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -67,8 +67,9 @@ fn is_not_modified( DateTime::parse_from_rfc2822(header_str) .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) .ok() - }).and_then(|t| t.with_nanosecond(0)); - Ok(max_created_at.is_some() && max_created_at <= last_modified) + }) + .and_then(|t| t.with_nanosecond(0)); + Ok(max_created_at.is_some() && max_created_at <= last_modified) } async fn generate_cac( diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index 044c6ed0b..d29760c40 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -42,4 +42,4 @@ pub enum ContextBulkResponse { PUT(PutResp), DELETE(String), MOVE(PutResp), -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 012680c69..120d78e0a 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -76,4 +76,4 @@ async fn create( .body("Failed to create DefaultConfig"); } } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/default_config/mod.rs b/crates/context-aware-config/src/api/default_config/mod.rs index 2e277d411..ebe17b924 100644 --- a/crates/context-aware-config/src/api/default_config/mod.rs +++ b/crates/context-aware-config/src/api/default_config/mod.rs @@ -1,3 +1,3 @@ mod handlers; mod types; -pub use handlers::endpoints; \ No newline at end of file +pub use handlers::endpoints; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index c64d13b9a..6e6639606 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -74,4 +74,4 @@ async fn create( .body("Failed to create/update dimension\n"); } } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context-aware-config/src/api/dimension/utils.rs index f62a43c47..8c24b25f7 100644 --- a/crates/context-aware-config/src/api/dimension/utils.rs +++ b/crates/context-aware-config/src/api/dimension/utils.rs @@ -2,8 +2,11 @@ use std::collections::HashMap; use crate::db::{models::Dimension, schema::dimensions::dsl::*}; use diesel::RunQueryDsl; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + PgConnection, +}; use jsonschema::{Draft, JSONSchema}; -use diesel::{ r2d2::{ConnectionManager, PooledConnection}, PgConnection }; pub fn get_all_dimension_schema_map( conn: &mut PooledConnection<ConnectionManager<PgConnection>>, @@ -23,4 +26,4 @@ pub fn get_all_dimension_schema_map( .collect(); Ok(dimension_schema_map) -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 53a0626c0..7ec6f2a88 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -53,4 +53,4 @@ pub struct EventLog { pub original_data: Option<Value>, pub new_data: Option<Value>, pub query: String, -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/lib.rs b/crates/context-aware-config/src/lib.rs index a86b33b08..851963c56 100644 --- a/crates/context-aware-config/src/lib.rs +++ b/crates/context-aware-config/src/lib.rs @@ -1,5 +1,4 @@ pub mod api; pub mod db; -pub mod middlewares; pub mod helpers; - +pub mod middlewares; diff --git a/crates/context-aware-config/src/middlewares/audit_response_header.rs b/crates/context-aware-config/src/middlewares/audit_response_header.rs index 34ef8ed58..0003c2694 100644 --- a/crates/context-aware-config/src/middlewares/audit_response_header.rs +++ b/crates/context-aware-config/src/middlewares/audit_response_header.rs @@ -3,15 +3,15 @@ use std::future::{ready, Ready}; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, http::header::{HeaderName, HeaderValue}, - Error + Error, }; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use futures_util::future::LocalBoxFuture; use service_utils::service::types::DbConnection; use crate::db::schema::event_log::dsl as event_log; -use uuid::Uuid; use std::rc::Rc; +use uuid::Uuid; #[derive(Clone, Copy, Debug, strum_macros::Display)] #[strum(serialize_all = "snake_case")] @@ -98,4 +98,4 @@ where Ok(res) }) } -} \ No newline at end of file +} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index caa71c78a..078e0528c 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -62,12 +62,13 @@ async fn create( let mut variants = req.variants.to_vec(); let DbConnection(mut conn) = db_conn; - + // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant check_variant_types(&variants)?; - let unique_override_keys: Vec<String> = - extract_override_keys(&variants[0].overrides).into_iter().collect(); + let unique_override_keys: Vec<String> = extract_override_keys(&variants[0].overrides) + .into_iter() + .collect(); let unique_ids_of_variants_from_req: HashSet<&str> = HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); @@ -489,10 +490,11 @@ async fn update_overrides( let first_variant = variants.get(0).ok_or(err::BadRequest(ErrorResponse { message: "Variant not found in request".to_string(), - possible_fix: "Provide at least one entry in variant's list" - .to_string(), + possible_fix: "Provide at least one entry in variant's list".to_string(), }))?; - let override_keys = extract_override_keys(&first_variant.overrides).into_iter().collect(); + let override_keys = extract_override_keys(&first_variant.overrides) + .into_iter() + .collect(); // fetch the current variants of the experiment let experiment = experiments::experiments diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index 876e15fb6..a85939ea7 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -317,8 +317,6 @@ pub fn add_variant_dimension_to_ctx( } } -pub fn extract_override_keys( - overrides: &Map<String, Value>, -) -> HashSet<String> { +pub fn extract_override_keys(overrides: &Map<String, Value>) -> HashSet<String> { overrides.keys().map(String::from).collect() } diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation-platform/src/api/experiments/types.rs index 0bff31296..d4a1c0b5e 100644 --- a/crates/experimentation-platform/src/api/experiments/types.rs +++ b/crates/experimentation-platform/src/api/experiments/types.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc, NaiveDateTime}; +use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use service_utils::helpers::deserialize_stringified_list; @@ -184,4 +184,4 @@ pub struct AuditQueryFilters { pub username: Option<String>, pub count: Option<i64>, pub page: Option<i64>, -} \ No newline at end of file +} diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 46a426c1e..143ddfa86 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -210,4 +210,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2024m06, event_log_y2024m07, experiments, -); \ No newline at end of file +); diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs index 00077b027..560af3031 100644 --- a/crates/experimentation-platform/tests/experimentation_tests.rs +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -62,13 +62,19 @@ fn experiment_gen( #[test] fn test_duplicate_override_key_entries() { let override_keys = vec!["key1".to_string(), "key2".to_string(), "key1".to_string()]; - assert!(matches!(helpers::validate_override_keys(&override_keys), Err(AppError::BadArgument(_)))); + assert!(matches!( + helpers::validate_override_keys(&override_keys), + Err(AppError::BadArgument(_)) + )); } #[test] fn test_unique_override_key_entries() { let override_keys = vec!["key1".to_string(), "key2".to_string()]; - assert!(matches!(helpers::validate_override_keys(&override_keys), Ok(()))); + assert!(matches!( + helpers::validate_override_keys(&override_keys), + Ok(()) + )); } #[test] @@ -572,4 +578,4 @@ fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlappi ); Ok(()) -} \ No newline at end of file +} diff --git a/crates/external/src/api/external_api/mod.rs b/crates/external/src/api/external_api/mod.rs index 61d779e04..d61ff8763 100644 --- a/crates/external/src/api/external_api/mod.rs +++ b/crates/external/src/api/external_api/mod.rs @@ -1,4 +1,4 @@ pub mod handlers; pub mod helpers; pub mod types; -pub use handlers::endpoints; \ No newline at end of file +pub use handlers::endpoints; diff --git a/crates/external/src/api/external_api/types.rs b/crates/external/src/api/external_api/types.rs index 091960d5c..ef3c588d2 100644 --- a/crates/external/src/api/external_api/types.rs +++ b/crates/external/src/api/external_api/types.rs @@ -4,5 +4,5 @@ use serde_json::Value; #[derive(Serialize)] pub struct DiffResponse { pub before: Value, - pub after: Value -} \ No newline at end of file + pub after: Value, +} diff --git a/crates/external/src/api/mod.rs b/crates/external/src/api/mod.rs index daacf5f5b..756165ffc 100644 --- a/crates/external/src/api/mod.rs +++ b/crates/external/src/api/mod.rs @@ -1 +1 @@ -pub mod external_api; \ No newline at end of file +pub mod external_api; diff --git a/crates/service-utils/src/aws/kms.rs b/crates/service-utils/src/aws/kms.rs index d42c9e132..6c4788f99 100644 --- a/crates/service-utils/src/aws/kms.rs +++ b/crates/service-utils/src/aws/kms.rs @@ -36,7 +36,8 @@ pub fn new_client() -> KmsClient { let kms_region = match app_env.as_str() { "DEV" => Region::Custom { name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), - endpoint: get_from_env_unsafe("AWS_REGION_ENDPOINT").unwrap_or(String::from("http://localhost:4566")), + endpoint: get_from_env_unsafe("AWS_REGION_ENDPOINT") + .unwrap_or(String::from("http://localhost:4566")), }, _ => get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1), }; diff --git a/crates/service-utils/src/db/pgschema_manager.rs b/crates/service-utils/src/db/pgschema_manager.rs index 2c9039b62..f9e61dbc3 100644 --- a/crates/service-utils/src/db/pgschema_manager.rs +++ b/crates/service-utils/src/db/pgschema_manager.rs @@ -2,11 +2,11 @@ extern crate derive_more; use derive_more::{Deref, DerefMut, Display}; use std::collections::HashMap; +use anyhow::anyhow; use diesel::{ r2d2::{ConnectionManager, Pool, PooledConnection}, PgConnection, }; -use anyhow::anyhow; pub type PgSchemaConnectionPool = Pool<ConnectionManager<PgConnection>>; pub type PgSchemaConnection = PooledConnection<ConnectionManager<PgConnection>>; @@ -73,4 +73,4 @@ impl PgSchemaManager { .get()?; // fetches the connection from the pool Ok(conn) } -} \ No newline at end of file +} diff --git a/crates/service-utils/src/db/utils.rs b/crates/service-utils/src/db/utils.rs index 1a98b8661..54405f4ef 100644 --- a/crates/service-utils/src/db/utils.rs +++ b/crates/service-utils/src/db/utils.rs @@ -1,6 +1,6 @@ use crate::aws::kms; use crate::db::pgschema_manager::{ConnectionConfig, PgSchemaManager}; -use crate::helpers::{get_from_env_unsafe, get_from_env_or_default}; +use crate::helpers::{get_from_env_or_default, get_from_env_unsafe}; use crate::service::types::AppEnv; use diesel::{ r2d2::{ConnectionManager, Pool}, @@ -62,4 +62,4 @@ pub async fn init_pool_manager( .collect::<Vec<ConnectionConfig>>(); PgSchemaManager::from(connection_configs) -} \ No newline at end of file +} diff --git a/crates/service-utils/src/errors/mod.rs b/crates/service-utils/src/errors/mod.rs index dd198c6d0..cd408564e 100644 --- a/crates/service-utils/src/errors/mod.rs +++ b/crates/service-utils/src/errors/mod.rs @@ -1 +1 @@ -pub mod types; \ No newline at end of file +pub mod types; diff --git a/crates/service-utils/src/errors/types.rs b/crates/service-utils/src/errors/types.rs index e08981939..c513d16fe 100644 --- a/crates/service-utils/src/errors/types.rs +++ b/crates/service-utils/src/errors/types.rs @@ -148,4 +148,4 @@ impl error::ResponseError for Error { Error::Generic(server_error) => server_error.error_response(), } } -} \ No newline at end of file +} diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 3a6a5a12a..e808e3923 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -1,7 +1,11 @@ use actix_web::{error::ErrorInternalServerError, Error}; use log::info; use serde::de::{self, IntoDeserializer}; -use std::{env::VarError, fmt::{self, Display}, str::FromStr}; +use std::{ + env::VarError, + fmt::{self, Display}, + str::FromStr, +}; //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should @@ -27,7 +31,9 @@ where match std::env::var(name) { Ok(env) => env.parse().unwrap(), Err(err) => { - info!("{name} ENV failed to load due to {err}, using default value {default}"); + info!( + "{name} ENV failed to load due to {err}, using default value {default}" + ); default } } diff --git a/crates/service-utils/src/middlewares/app_scope.rs b/crates/service-utils/src/middlewares/app_scope.rs index 0dd8778a1..6f8362ed8 100644 --- a/crates/service-utils/src/middlewares/app_scope.rs +++ b/crates/service-utils/src/middlewares/app_scope.rs @@ -67,4 +67,4 @@ where Ok(res) }) } -} \ No newline at end of file +} diff --git a/crates/service-utils/src/middlewares/mod.rs b/crates/service-utils/src/middlewares/mod.rs index d842a10db..2e5d06b8a 100644 --- a/crates/service-utils/src/middlewares/mod.rs +++ b/crates/service-utils/src/middlewares/mod.rs @@ -1,2 +1,2 @@ pub mod app_scope; -pub mod tenant; \ No newline at end of file +pub mod tenant; diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index b9c1ceab9..86e6e89b6 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -2,9 +2,10 @@ use std::future::{ready, Ready}; use crate::service::types::{AppState, Tenant}; use actix_web::{ - web::Data, error, - http::header::HeaderValue, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + error, + http::header::HeaderValue, + web::Data, Error, HttpMessage, }; use futures_util::future::LocalBoxFuture; @@ -24,7 +25,9 @@ where type Future = Ready<Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(TenantMiddleware { service: Rc::new(service) })) + ready(Ok(TenantMiddleware { + service: Rc::new(service), + })) } } @@ -52,25 +55,29 @@ where Some(val) => val, None => { log::error!("app state not set"); - return Err(error::ErrorInternalServerError("")) + return Err(error::ErrorInternalServerError("")); } }; let request_path = req.uri().path(); - let is_excluded: bool = app_state.tenant_middleware_exclusion_list.contains(request_path); + let is_excluded: bool = app_state + .tenant_middleware_exclusion_list + .contains(request_path); if !is_excluded && app_state.enable_tenant_and_scope { let tenant = req .headers() .get("x-tenant") - .map_or(None, |header_value: &HeaderValue| header_value.to_str().ok()) + .map_or(None, |header_value: &HeaderValue| { + header_value.to_str().ok() + }) .map(|header_str| header_str.to_string()); let validated_tenant: Tenant = match tenant { Some(val) if app_state.tenants.contains(&val) => Tenant(val), Some(_) => { return Err(error::ErrorBadRequest("invalid x-tenant value")); - }, + } None => { return Err(error::ErrorBadRequest("x-tenant not set")); } @@ -84,4 +91,4 @@ where Ok(res) }) } -} \ No newline at end of file +} diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 039942b40..8eb569178 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -2,6 +2,7 @@ mod types; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; +use derive_more::{Deref, DerefMut}; use serde_json::Value; use tokio::{ sync::RwLock, @@ -9,7 +10,6 @@ use tokio::{ }; pub use types::{Config, Experiment, Experiments, Variants}; use types::{ExperimentStore, ListExperimentsResponse, Variant, VariantType}; -use derive_more::{Deref, DerefMut}; #[derive(Clone, Debug)] pub struct Client { @@ -166,7 +166,7 @@ async fn get_experiments( } #[derive(Deref, DerefMut)] -pub struct ClientFactory ( RwLock<HashMap<String, Arc<Client>>> ); +pub struct ClientFactory(RwLock<HashMap<String, Arc<Client>>>); impl ClientFactory { pub async fn create_client( &self, @@ -180,13 +180,11 @@ impl ClientFactory { return Ok(client.clone()); } - let client = Arc::new( - Client::new(Config { - tenant: tenant.to_string(), - hostname: hostname, - poll_frequency: poll_frequency - }) - ); + let client = Arc::new(Client::new(Config { + tenant: tenant.to_string(), + hostname: hostname, + poll_frequency: poll_frequency, + })); factory.insert(tenant.to_string(), client.clone()); return Ok(client.clone()); @@ -196,12 +194,11 @@ impl ClientFactory { let factory = self.read().await; match factory.get(&tenant) { Some(client) => Ok(client.clone()), - None => Err("No such tenant found".to_string()) + None => Err("No such tenant found".to_string()), } } } use once_cell::sync::Lazy; -pub static CLIENT_FACTORY: Lazy<ClientFactory> = Lazy::new(|| { - ClientFactory(RwLock::new(HashMap::new())) -}); \ No newline at end of file +pub static CLIENT_FACTORY: Lazy<ClientFactory> = + Lazy::new(|| ClientFactory(RwLock::new(HashMap::new()))); diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index a0948d7af..a53c668a0 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -51,4 +51,4 @@ pub(crate) struct ListExperimentsResponse { pub(crate) total_items: i64, pub(crate) total_pages: i64, pub(crate) data: Experiments, -} \ No newline at end of file +} diff --git a/crates/superposition_client_integration_example/src/main.rs b/crates/superposition_client_integration_example/src/main.rs index 8733efad1..8a1bae13c 100644 --- a/crates/superposition_client_integration_example/src/main.rs +++ b/crates/superposition_client_integration_example/src/main.rs @@ -44,4 +44,4 @@ async fn get_variants( let variant = state.get_applicable_variant(&contexts, toss).await; println!("variant value: {:?}", variant); HttpResponse::Ok().body("check your console") -} \ No newline at end of file +} From bb87c89fdbe7222954739c840a3a4377c3e5bbfb Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Tue, 10 Oct 2023 18:10:19 +0530 Subject: [PATCH 180/352] chore: move dependencies to workspaces --- Cargo.lock | 1338 +++-------------- Cargo.toml | 31 +- crates/cac_client/Cargo.toml | 20 +- crates/context-aware-config/Cargo.toml | 48 +- crates/experimentation-platform/Cargo.toml | 30 +- crates/external/Cargo.toml | 18 +- .../external/src/api/external_api/handlers.rs | 4 +- .../external/src/api/external_api/helpers.rs | 7 +- crates/service-utils/Cargo.toml | 38 +- crates/superposition_client/Cargo.toml | 18 +- .../Cargo.toml | 8 +- 11 files changed, 361 insertions(+), 1199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb0120a59..7b1e81370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "actix-rt", "actix_derive", "bitflags 1.3.2", - "bytes 1.4.0", + "bytes", "crossbeam-channel", "futures-core", "futures-sink", @@ -19,10 +19,10 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", - "smallvec 1.10.0", - "tokio 1.29.1", + "smallvec", + "tokio", "tokio-util", ] @@ -33,13 +33,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" dependencies = [ "bitflags 1.3.2", - "bytes 1.4.0", + "bytes", "futures-core", "futures-sink", "log", "memchr", "pin-project-lite", - "tokio 1.29.1", + "tokio", "tokio-util", ] @@ -55,7 +55,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "smallvec 1.10.0", + "smallvec", ] [[package]] @@ -70,14 +70,14 @@ dependencies = [ "actix-web", "askama_escape", "bitflags 1.3.2", - "bytes 1.4.0", + "bytes", "derive_more", "futures-core", "http-range", "log", "mime", "mime_guess", - "percent-encoding 2.2.0", + "percent-encoding", "pin-project-lite", ] @@ -95,26 +95,26 @@ dependencies = [ "base64 0.21.2", "bitflags 1.3.2", "brotli", - "bytes 1.4.0", + "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", "futures-core", - "h2 0.3.18", - "http 0.2.9", + "h2", + "http", "httparse", "httpdate", - "itoa 1.0.6", + "itoa", "language-tags", "local-channel", "mime", - "percent-encoding 2.2.0", + "percent-encoding", "pin-project-lite", - "rand 0.8.5", + "rand", "sha1", - "smallvec 1.10.0", - "tokio 1.29.1", + "smallvec", + "tokio", "tokio-util", "tracing", "zstd", @@ -137,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" dependencies = [ "bytestring", - "http 0.2.9", + "http", "regex", "serde", "tracing", @@ -150,7 +150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" dependencies = [ "futures-core", - "tokio 1.29.1", + "tokio", ] [[package]] @@ -164,10 +164,10 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 0.8.6", + "mio", "num_cpus", "socket2", - "tokio 1.29.1", + "tokio", "tracing", ] @@ -208,16 +208,16 @@ dependencies = [ "actix-utils", "actix-web-codegen", "ahash 0.7.6", - "bytes 1.4.0", + "bytes", "bytestring", - "cfg-if 1.0.0", - "cookie 0.16.2", + "cfg-if", + "cookie", "derive_more", "encoding_rs", "futures-core", "futures-util", - "http 0.2.9", - "itoa 1.0.6", + "http", + "itoa", "language-tags", "log", "mime", @@ -226,11 +226,11 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded 0.7.1", - "smallvec 1.10.0", + "serde_urlencoded", + "smallvec", "socket2", "time 0.3.21", - "url 2.3.1", + "url", ] [[package]] @@ -288,7 +288,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "getrandom", "once_cell", "serde", @@ -465,16 +465,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi 0.3.9", -] - -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.1.0", + "winapi", ] [[package]] @@ -491,22 +482,13 @@ checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide 0.6.2", "object", "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder", -] - [[package]] name = "base64" version = "0.13.1" @@ -555,7 +537,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if 1.0.0", + "cfg-if", "constant_time_eq", "digest 0.10.6", ] @@ -617,17 +599,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "either", - "iovec", -] - [[package]] name = "bytes" version = "1.4.0" @@ -640,7 +611,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" dependencies = [ - "bytes 1.4.0", + "bytes", ] [[package]] @@ -654,7 +625,7 @@ dependencies = [ "jsonlogic", "log", "once_cell", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", ] @@ -668,12 +639,12 @@ dependencies = [ "async-trait", "cached_proc_macro", "cached_proc_macro_types", - "futures 0.3.28", + "futures", "hashbrown 0.13.2", "instant", "once_cell", "thiserror", - "tokio 1.29.1", + "tokio", ] [[package]] @@ -710,12 +681,6 @@ dependencies = [ "jobserver", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -735,7 +700,7 @@ dependencies = [ "serde", "time 0.1.45", "wasm-bindgen", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -807,15 +772,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -869,7 +825,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen", ] @@ -911,7 +867,7 @@ dependencies = [ "anyhow", "base64 0.21.2", "blake3", - "bytes 1.4.0", + "bytes", "cac_client", "chrono", "dashboard-auth", @@ -923,7 +879,7 @@ dependencies = [ "experimentation-platform", "external", "frontend", - "futures 0.3.28", + "futures", "futures-util", "json-patch", "jsonschema", @@ -932,8 +888,8 @@ dependencies = [ "leptos_meta", "leptos_router", "log", - "rand 0.8.5", - "reqwest 0.9.24", + "rand", + "reqwest", "rs-snowflake", "rusoto_core", "rusoto_kms", @@ -948,7 +904,7 @@ dependencies = [ "tracing-log", "tracing-subscriber", "urlencoding", - "uuid 1.3.4", + "uuid", "valuable", ] @@ -967,45 +923,17 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cookie" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" -dependencies = [ - "time 0.1.45", - "url 1.7.2", -] - [[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "percent-encoding 2.2.0", + "percent-encoding", "time 0.3.21", "version_check", ] -[[package]] -name = "cookie_store" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46750b3f362965f197996c4448e4a0935e791bf7d6631bfce9ee0af3d24c919c" -dependencies = [ - "cookie 0.12.0", - "failure", - "idna 0.1.5", - "log", - "publicsuffix", - "serde", - "serde_json", - "time 0.1.45", - "try_from", - "url 1.7.2", -] - [[package]] name = "core-foundation" version = "0.9.3" @@ -1037,7 +965,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1046,56 +974,8 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils 0.8.15", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -dependencies = [ - "autocfg 1.1.0", - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -dependencies = [ - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg 1.1.0", - "cfg-if 0.1.10", - "lazy_static", + "cfg-if", + "crossbeam-utils", ] [[package]] @@ -1104,7 +984,7 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1127,15 +1007,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -dependencies = [ - "sct", -] - [[package]] name = "cxx" version = "1.0.94" @@ -1227,7 +1098,7 @@ dependencies = [ "env_logger 0.10.0", "futures-util", "log", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", ] @@ -1252,7 +1123,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 1.0.109", ] @@ -1266,11 +1137,11 @@ dependencies = [ "byteorder", "chrono", "diesel_derives", - "itoa 1.0.6", + "itoa", "pq-sys", "r2d2", "serde_json", - "uuid 1.3.4", + "uuid", ] [[package]] @@ -1332,7 +1203,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "dirs-sys-next", ] @@ -1344,7 +1215,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1365,12 +1236,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - [[package]] name = "educe" version = "0.4.23" @@ -1395,7 +1260,7 @@ version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1498,14 +1363,12 @@ dependencies = [ "dotenv", "env_logger 0.8.4", "log", - "reqwest 0.11.20", + "reqwest", "rs-snowflake", "serde", "serde_json", "service-utils", - "strum", - "strum_macros", - "uuid 1.3.4", + "uuid", ] [[package]] @@ -1519,34 +1382,12 @@ dependencies = [ "dotenv", "experimentation-platform", "log", - "reqwest 0.9.24", + "reqwest", "serde", "serde_json", "service-utils", ] -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] - [[package]] name = "fancy-regex" version = "0.11.0" @@ -1603,7 +1444,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "percent-encoding 2.2.0", + "percent-encoding", ] [[package]] @@ -1622,49 +1463,21 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-web", - "cfg-if 1.0.0", + "cfg-if", "console_error_panic_hook", - "futures 0.3.28", - "http 0.2.9", + "futures", + "http", "leptos", "leptos_actix", "leptos_meta", "leptos_router", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", "wasm-bindgen", "web-sys", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags 1.3.2", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - -[[package]] -name = "futures" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" - [[package]] name = "futures" version = "0.3.28" @@ -1696,16 +1509,6 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" -[[package]] -name = "futures-cpupool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" -dependencies = [ - "futures 0.1.31", - "num_cpus", -] - [[package]] name = "futures-executor" version = "0.3.28" @@ -1780,7 +1583,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1826,39 +1629,21 @@ dependencies = [ "web-sys", ] -[[package]] -name = "h2" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" -dependencies = [ - "byteorder", - "bytes 0.4.12", - "fnv", - "futures 0.1.31", - "http 0.1.21", - "indexmap 1.9.3", - "log", - "slab", - "string", - "tokio-io", -] - [[package]] name = "h2" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.9", + "http", "indexmap 1.9.3", "slab", - "tokio 1.29.1", + "tokio", "tokio-util", "tracing", ] @@ -1948,38 +1733,15 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa 0.4.8", -] - [[package]] name = "http" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", - "itoa 1.0.6", -] - -[[package]] -name = "http-body" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "http 0.1.21", - "tokio-buf", + "itoa", ] [[package]] @@ -1988,8 +1750,8 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.4.0", - "http 0.2.9", + "bytes", + "http", "pin-project-lite", ] @@ -2017,88 +1779,42 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "hyper" -version = "0.12.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c843caf6296fc1f93444735205af9ed4e109a539005abb2564ae1d6fad34c52" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "futures-cpupool", - "h2 0.1.26", - "http 0.1.21", - "http-body 0.1.0", - "httparse", - "iovec", - "itoa 0.4.8", - "log", - "net2", - "rustc_version 0.2.3", - "time 0.1.45", - "tokio 0.1.22", - "tokio-buf", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "tokio-tcp", - "tokio-threadpool", - "tokio-timer", - "want 0.2.0", -] - [[package]] name = "hyper" version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.3.18", - "http 0.2.9", - "http-body 0.4.5", + "h2", + "http", + "http-body", "httparse", "httpdate", - "itoa 1.0.6", + "itoa", "pin-project-lite", "socket2", - "tokio 1.29.1", + "tokio", "tower-service", "tracing", - "want 0.3.0", + "want", ] [[package]] name = "hyper-rustls" -version = "0.17.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d85c7df4a7f309a77d145340a063ea929dcb2e025bae46a80345cffec2952" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes 0.4.12", - "ct-logs", - "futures 0.1.31", - "hyper 0.12.36", + "futures-util", + "http", + "hyper", "rustls", - "tokio-io", + "tokio", "tokio-rustls", - "webpki", - "webpki-roots", -] - -[[package]] -name = "hyper-tls" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "hyper 0.12.36", - "native-tls", - "tokio-io", ] [[package]] @@ -2107,10 +1823,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.4.0", - "hyper 0.14.26", + "bytes", + "hyper", "native-tls", - "tokio 1.29.1", + "tokio", "tokio-native-tls", ] @@ -2144,28 +1860,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.3.0" @@ -2182,7 +1876,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg 1.1.0", + "autocfg", "hashbrown 0.12.3", ] @@ -2202,7 +1896,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -2228,15 +1922,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" version = "2.7.2" @@ -2273,12 +1958,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.6" @@ -2350,29 +2029,19 @@ dependencies = [ "fraction", "getrandom", "iso8601", - "itoa 1.0.6", + "itoa", "memchr", "num-cmp", "once_cell", - "parking_lot 0.12.1", - "percent-encoding 2.2.0", + "parking_lot", + "percent-encoding", "regex", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", "time 0.3.21", - "url 2.3.1", - "uuid 1.3.4", -] - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", + "url", + "uuid", ] [[package]] @@ -2393,7 +2062,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65154cd0fc2f505a1676b870d5c055dec9dafe4d6081358ef1d7e357d6f222c5" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "leptos_config", "leptos_dom", "leptos_macro", @@ -2412,12 +2081,12 @@ checksum = "a940095989acffbd08f0264e4c7c529a388adbada7dadf26372148c66f221995" dependencies = [ "actix-http", "actix-web", - "futures 0.3.28", + "futures", "leptos", "leptos_integration_utils", "leptos_meta", "leptos_router", - "parking_lot 0.12.1", + "parking_lot", "regex", "serde_json", "tracing", @@ -2443,10 +2112,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5a92b7a30d6e1363233211babdd59fdd983f28dc3aa6aebbd7bfbdd15630c73" dependencies = [ "async-recursion", - "cfg-if 1.0.0", + "cfg-if", "drain_filter_polyfill", "educe", - "futures 0.3.28", + "futures", "getrandom", "html-escape", "indexmap 2.0.2", @@ -2460,7 +2129,7 @@ dependencies = [ "serde", "serde_json", "server_fn", - "smallvec 1.10.0", + "smallvec", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -2476,7 +2145,7 @@ dependencies = [ "anyhow", "camino", "indexmap 2.0.2", - "parking_lot 0.12.1", + "parking_lot", "proc-macro2", "quote", "rstml", @@ -2491,7 +2160,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bdd4411a987054b1bce1f89c888ea1bfde50f9c956ce7edc0bb5b54deaf1621" dependencies = [ - "futures 0.3.28", + "futures", "leptos", "leptos_config", "leptos_hot_reload", @@ -2506,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cc27567e059d8ab630a33bf782a81bb2e10178011b8c97c080aafcf09c4e5e0" dependencies = [ "attribute-derive", - "cfg-if 1.0.0", + "cfg-if", "convert_case 0.6.0", "html-escape", "itertools", @@ -2519,7 +2188,7 @@ dependencies = [ "server_fn_macro", "syn 2.0.32", "tracing", - "uuid 1.3.4", + "uuid", ] [[package]] @@ -2528,7 +2197,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "329c4bbe4191a0bef6514bab827f2ff1a1e69bc2b431e78ac9799e2bdc26ae33" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "indexmap 2.0.2", "leptos", "tracing", @@ -2543,8 +2212,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b4fc821e6a8646635b721dd58b5604b5c447eb3b21c464b3837cd2063a6b209" dependencies = [ "base64 0.21.2", - "cfg-if 1.0.0", - "futures 0.3.28", + "cfg-if", + "futures", "indexmap 2.0.2", "js-sys", "rustc-hash", @@ -2554,7 +2223,7 @@ dependencies = [ "serde_json", "slotmap", "thiserror", - "tokio 1.29.1", + "tokio", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -2568,7 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f02384aaeff09ba17093305a0dfe8713fb171f7227a8543992a9ce44c75cd" dependencies = [ "cached", - "cfg-if 1.0.0", + "cfg-if", "common_macros", "gloo-net", "js-sys", @@ -2578,14 +2247,14 @@ dependencies = [ "log", "lru", "once_cell", - "percent-encoding 2.2.0", + "percent-encoding", "regex", "serde", "serde_json", "serde_qs", "thiserror", "tracing", - "url 2.3.1", + "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2662,22 +2331,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" -[[package]] -name = "lock_api" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ - "autocfg 1.1.0", + "autocfg", "scopeguard", ] @@ -2709,18 +2369,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "md-5" version = "0.9.1" @@ -2738,15 +2386,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "mime" version = "0.3.17" @@ -2787,25 +2426,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", -] - [[package]] name = "mio" version = "0.8.6" @@ -2818,18 +2438,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -2848,17 +2456,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "net2" -version = "0.2.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - [[package]] name = "nom" version = "7.1.3" @@ -2876,7 +2473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2899,7 +2496,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] @@ -2925,7 +2522,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-traits", ] @@ -2935,7 +2532,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] @@ -2946,7 +2543,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-bigint", "num-integer", "num-traits", @@ -2958,7 +2555,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -2999,7 +2596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.0", + "cfg-if", "foreign-types", "libc", "once_cell", @@ -3060,38 +2657,12 @@ checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" [[package]] name = "parking_lot" -version = "0.9.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.6.3", - "rustc_version 0.2.3", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api 0.4.9", - "parking_lot_core 0.9.7", -] - -[[package]] -name = "parking_lot_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "rustc_version 0.2.3", - "smallvec 0.6.14", - "winapi 0.3.9", + "lock_api", + "parking_lot_core", ] [[package]] @@ -3100,10 +2671,10 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall 0.2.16", - "smallvec 1.10.0", + "smallvec", "windows-sys 0.45.0", ] @@ -3119,12 +2690,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" - [[package]] name = "percent-encoding" version = "2.2.0" @@ -3271,7 +2836,7 @@ checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" dependencies = [ "proc-macro2", "quote", - "smallvec 1.10.0", + "smallvec", ] [[package]] @@ -3296,16 +2861,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "publicsuffix" -version = "1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" -dependencies = [ - "idna 0.2.3", - "url 2.3.1", -] - [[package]] name = "quote" version = "1.0.32" @@ -3345,29 +2900,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot 0.12.1", + "parking_lot", "scheduled-thread-pool", ] -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.8", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift", - "winapi 0.3.9", -] - [[package]] name = "rand" version = "0.8.5" @@ -3375,18 +2911,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -3396,24 +2922,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -3423,83 +2934,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi 0.3.9", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi 0.3.9", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_syscall" version = "0.2.16" @@ -3561,44 +2995,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" -[[package]] -name = "reqwest" -version = "0.9.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" -dependencies = [ - "base64 0.10.1", - "bytes 0.4.12", - "cookie 0.12.0", - "cookie_store", - "encoding_rs", - "flate2", - "futures 0.1.31", - "http 0.1.21", - "hyper 0.12.36", - "hyper-rustls", - "hyper-tls 0.3.2", - "log", - "mime", - "mime_guess", - "native-tls", - "rustls", - "serde", - "serde_json", - "serde_urlencoded 0.5.5", - "time 0.1.45", - "tokio 0.1.22", - "tokio-executor", - "tokio-io", - "tokio-rustls", - "tokio-threadpool", - "tokio-timer", - "url 1.7.2", - "uuid 0.7.4", - "webpki-roots", - "winreg 0.6.2", -] - [[package]] name = "reqwest" version = "0.11.20" @@ -3606,34 +3002,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.2", - "bytes 1.4.0", + "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.18", - "http 0.2.9", - "http-body 0.4.5", - "hyper 0.14.26", - "hyper-tls 0.5.0", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", - "percent-encoding 2.2.0", + "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", - "serde_urlencoded 0.7.1", - "tokio 1.29.1", + "serde_urlencoded", + "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", - "url 2.3.1", + "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "webpki-roots", + "winreg", ] [[package]] @@ -3648,7 +3049,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3690,20 +3091,20 @@ checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" dependencies = [ "async-trait", "base64 0.13.1", - "bytes 1.4.0", + "bytes", "crc32fast", - "futures 0.3.28", - "http 0.2.9", - "hyper 0.14.26", - "hyper-tls 0.5.0", + "futures", + "http", + "hyper", + "hyper-tls", "lazy_static", "log", "rusoto_credential", "rusoto_signature", - "rustc_version 0.4.0", + "rustc_version", "serde", "serde_json", - "tokio 1.29.1", + "tokio", "xml-rs", ] @@ -3716,12 +3117,12 @@ dependencies = [ "async-trait", "chrono", "dirs-next", - "futures 0.3.28", - "hyper 0.14.26", + "futures", + "hyper", "serde", "serde_json", "shlex", - "tokio 1.29.1", + "tokio", "zeroize", ] @@ -3732,8 +3133,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" dependencies = [ "async-trait", - "bytes 1.4.0", - "futures 0.3.28", + "bytes", + "futures", "rusoto_core", "serde", "serde_json", @@ -3746,23 +3147,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" dependencies = [ "base64 0.13.1", - "bytes 1.4.0", + "bytes", "chrono", "digest 0.9.0", - "futures 0.3.28", + "futures", "hex", "hmac", - "http 0.2.9", - "hyper 0.14.26", + "http", + "hyper", "log", "md-5", - "percent-encoding 2.2.0", + "percent-encoding", "pin-project-lite", "rusoto_credential", - "rustc_version 0.4.0", + "rustc_version", "serde", "sha2 0.9.9", - "tokio 1.29.1", + "tokio", ] [[package]] @@ -3771,7 +3172,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "ordered-multimap", ] @@ -3787,22 +3188,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.17", + "semver", ] [[package]] @@ -3821,15 +3213,33 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "base64 0.10.1", "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -3868,7 +3278,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot 0.12.1", + "parking_lot", ] [[package]] @@ -3885,9 +3295,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3922,27 +3332,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.180" @@ -3989,7 +3384,7 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ - "itoa 1.0.6", + "itoa", "ryu", "serde", ] @@ -4000,7 +3395,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" dependencies = [ - "percent-encoding 2.2.0", + "percent-encoding", "serde", "thiserror", ] @@ -4014,18 +3409,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" -dependencies = [ - "dtoa", - "itoa 0.4.8", - "serde", - "url 1.7.2", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4033,7 +3416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.6", + "itoa", "ryu", "serde", ] @@ -4053,7 +3436,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", "serde_qs", @@ -4096,7 +3479,7 @@ dependencies = [ "actix-web", "anyhow", "base64 0.21.2", - "bytes 1.4.0", + "bytes", "dashboard-auth", "derive_more", "diesel", @@ -4122,7 +3505,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.6", ] @@ -4134,7 +3517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -4146,7 +3529,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.6", ] @@ -4181,7 +3564,7 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -4194,15 +3577,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "smallvec" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" -dependencies = [ - "maybe-uninit", -] - [[package]] name = "smallvec" version = "1.10.0" @@ -4216,7 +3590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4225,15 +3599,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "string" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" -dependencies = [ - "bytes 0.4.12", -] - [[package]] name = "strsim" version = "0.10.0" @@ -4275,10 +3640,10 @@ dependencies = [ "jsonlogic", "log", "once_cell", - "reqwest 0.11.20", + "reqwest", "serde", "serde_json", - "tokio 1.29.1", + "tokio", ] [[package]] @@ -4312,7 +3677,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.6", + "itoa", "ryu", "sval", ] @@ -4323,7 +3688,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.6", + "itoa", "ryu", "sval", ] @@ -4383,26 +3748,14 @@ dependencies = [ "syn 2.0.32", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - [[package]] name = "tempfile" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ - "autocfg 1.1.0", - "cfg-if 1.0.0", + "autocfg", + "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", @@ -4444,7 +3797,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -4456,7 +3809,7 @@ checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4465,7 +3818,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ - "itoa 1.0.6", + "itoa", "serde", "time-core", "time-macros", @@ -4501,38 +3854,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "mio 0.6.23", - "num_cpus", - "tokio-current-thread", - "tokio-executor", - "tokio-io", - "tokio-reactor", - "tokio-tcp", - "tokio-threadpool", - "tokio-timer", -] - [[package]] name = "tokio" version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ - "autocfg 1.1.0", + "autocfg", "backtrace", - "bytes 1.4.0", + "bytes", "libc", - "mio 0.8.6", + "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4540,48 +3874,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "tokio-buf" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" -dependencies = [ - "bytes 0.4.12", - "either", - "futures 0.1.31", -] - -[[package]] -name = "tokio-current-thread" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" -dependencies = [ - "futures 0.1.31", - "tokio-executor", -] - -[[package]] -name = "tokio-executor" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" -dependencies = [ - "crossbeam-utils 0.7.2", - "futures 0.1.31", -] - -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "log", -] - [[package]] name = "tokio-macros" version = "2.1.0" @@ -4600,93 +3892,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.29.1", -] - -[[package]] -name = "tokio-reactor" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" -dependencies = [ - "crossbeam-utils 0.7.2", - "futures 0.1.31", - "lazy_static", - "log", - "mio 0.6.23", - "num_cpus", - "parking_lot 0.9.0", - "slab", - "tokio-executor", - "tokio-io", - "tokio-sync", + "tokio", ] [[package]] name = "tokio-rustls" -version = "0.10.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7cf08f990090abd6c6a73cab46fed62f85e8aef8b99e4b918a9f4a637f0676" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", "rustls", - "tokio-io", - "webpki", -] - -[[package]] -name = "tokio-sync" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" -dependencies = [ - "fnv", - "futures 0.1.31", -] - -[[package]] -name = "tokio-tcp" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "iovec", - "mio 0.6.23", - "tokio-io", - "tokio-reactor", -] - -[[package]] -name = "tokio-threadpool" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" -dependencies = [ - "crossbeam-deque", - "crossbeam-queue", - "crossbeam-utils 0.7.2", - "futures 0.1.31", - "lazy_static", - "log", - "num_cpus", - "slab", - "tokio-executor", -] - -[[package]] -name = "tokio-timer" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" -dependencies = [ - "crossbeam-utils 0.7.2", - "futures 0.1.31", - "slab", - "tokio-executor", + "tokio", ] [[package]] @@ -4695,11 +3911,11 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite", - "tokio 1.29.1", + "tokio", "tracing", ] @@ -4724,7 +3940,7 @@ version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -4740,7 +3956,7 @@ dependencies = [ "actix-web", "pin-project", "tracing", - "uuid 1.3.4", + "uuid", ] [[package]] @@ -4798,7 +4014,7 @@ dependencies = [ "serde", "serde_json", "sharded-slab", - "smallvec 1.10.0", + "smallvec", "thread_local", "tracing", "tracing-core", @@ -4821,15 +4037,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "try_from" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" -dependencies = [ - "cfg-if 0.1.10", -] - [[package]] name = "typed-builder" version = "0.14.0" @@ -4907,17 +4114,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna 0.1.5", - "matches", - "percent-encoding 1.0.1", -] - [[package]] name = "url" version = "2.3.1" @@ -4925,8 +4121,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna 0.3.0", - "percent-encoding 2.2.0", + "idna", + "percent-encoding", ] [[package]] @@ -4947,15 +4143,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" -[[package]] -name = "uuid" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" -dependencies = [ - "rand 0.6.5", -] - [[package]] name = "uuid" version = "1.3.4" @@ -5044,17 +4231,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" -dependencies = [ - "futures 0.1.31", - "log", - "try-lock", -] - [[package]] name = "want" version = "0.3.0" @@ -5083,7 +4259,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] @@ -5108,7 +4284,7 @@ version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -5153,30 +4329,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.17.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262ae37dd9d60f60dd473d1158f9fbebf110ba7b6a5051c8160460f6043718b" -dependencies = [ - "webpki", -] - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "winapi" @@ -5188,12 +4345,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -5206,7 +4357,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5371,35 +4522,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "windows-sys 0.48.0", ] -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "xml-rs" version = "0.8.14" diff --git a/Cargo.toml b/Cargo.toml index 9f33eb347..e8447bbca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,33 @@ bin-package = "context-aware-config" lib-package = "frontend" assets-dir = "crates/frontend/assets" style-file = "frontend/style/tailwind.css" -site-root = "crates/frontend" \ No newline at end of file +site-root = "crates/frontend" + +[workspace.dependencies] +dotenv = "0.15.0" +actix = "0.13.0" +actix-web = "4.0.0" +diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +env_logger = "0.8" +log = { version="0.4.20", features = ["kv_unstable_serde"] } +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +derive_more = "^0.99" +base64 = "0.21.2" +urlencoding = "~2.1.2" +chrono = { version = "0.4.26", features = ["serde"] } +uuid = {version = "1.3.4", features = ["v4", "serde"]} +reqwest = { version = "0.11.18", features = ["json"]} +jsonschema = "~0.17" +jsonlogic = "0.5.1" +json-patch = "1.0.0" +rs-snowflake = "0.6.0" +rusoto_kms = "0.48.0" +rusoto_signature = "0.48.0" +bytes = "1.4.0" +rusoto_core = "0.48.0" +rand = "0.8.5" +once_cell = { version = "1.18.0" } +anyhow = { version = "1.0", default-features = false } +# juspay dependencies +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 10a30082c..b0e3e0119 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -6,13 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -actix-web = "4.3.1" -chrono = "0.4.26" -jsonlogic = "0.5.1" -reqwest = { version = "0.11.18", features = ["json"]} -once_cell = { version = "1.18.0" } -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.97" -log = "^0.4" -json-patch = "1.0.0" -derive_more = "^0.99" +once_cell = { workspace = true } +derive_more = { workspace = true } +actix-web = { workspace = true } +chrono = { workspace = true } +jsonlogic = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } +json-patch = { workspace = true } diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 3fb5bc407..28b007fb4 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -11,40 +11,40 @@ cac_client = { path = "../cac_client" } frontend = {path ="../frontend" } # env -dotenv = "0.15.0" +dotenv = { workspace = true } # Https server framework -actix = "0.13.0" -actix-web = { version = "4.0.0", features = ["macros"] } +actix = { workspace = true } +actix-web = { workspace = true } # To help generate snowflake ids -rs-snowflake = "0.6.0" +rs-snowflake = { workspace = true } # To help with generating uuids -uuid = {version = "1.3.4", features = ["v4", "serde"]} +uuid = { workspace = true } # To serialize and deserialize objects from json -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} +serde = { workspace = true } +serde_json = { workspace = true } # For logging and debugging -env_logger = "0.8" -log = { version="0.4.20", features = ["kv_unstable_serde"] } +env_logger = { workspace = true } +log = { workspace = true } # to work with enums strum_macros = "^0.24" strum = {version = "^0.24"} -derive_more = "^0.99" +derive_more = { workspace = true } # date and time -chrono = { version = "0.4", features = ["serde"] } +chrono = { workspace = true } # ORM -diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +diesel = { workspace = true } blake3 = "1.3.3" -rusoto_kms = "0.48.0" -rusoto_signature = "0.48.0" -bytes = "1.4.0" -rusoto_core = "0.48.0" -base64 = "0.21.2" +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } -urlencoding = "~2.1.2" -jsonschema = "~0.17" -reqwest = { version = "*", features = [ "rustls-tls" ] } -json-patch = "1.0.0" -rand = "0.8.5" +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true , features = [ "rustls-tls" ] } +json-patch = { workspace = true } +rand = { workspace = true } service-utils = { path = "../service-utils" } experimentation-platform = { path = "../experimentation-platform"} tracing-actix-web = "0.7.6" @@ -57,11 +57,11 @@ actix-http = "3.3.1" futures-util = "0.3.28" external = { path = "../external"} actix-cors = "0.6.4" -anyhow = { version = "1.0", default-features = false } -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} leptos_actix = { version = "0.4.10" } leptos = { version = "0.4" } leptos_meta = { version = "0.4" } leptos_router = { version = "0.4" } actix-files = { version = "0.6" } +anyhow = { workspace = true } +dashboard-auth = { workspace = true } diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index c67ebf641..d76800e15 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -7,29 +7,27 @@ edition = "2021" [dependencies] # env -dotenv = "0.15.0" +dotenv = { workspace = true } # Https server framework -actix = "0.13.0" -actix-web = "4.0.0" +actix = { workspace = true } +actix-web = { workspace = true } # To help generate snowflake ids -rs-snowflake = "0.6.0" +rs-snowflake = { workspace = true } # To help with generating uuids -uuid = {version = "1.3.4", features = ["v4", "serde"]} +uuid = { workspace = true } # To serialize and deserialize objects from json -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} +serde = { workspace = true } +serde_json = { workspace = true } # For logging and debugging -env_logger = "0.8" -log = "^0.4" +env_logger = { workspace = true } +log = { workspace = true } # to work with enums -strum_macros = "^0.24" -strum = {version = "^0.24"} -derive_more = "^0.99" +derive_more = { workspace = true } # date and time -chrono = { version = "0.4", features = ["serde"] } +chrono = { workspace = true } # ORM -diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +diesel = { workspace = true } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } -reqwest = {version = "0.11.18"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} +reqwest = { workspace = true } +dashboard-auth = { workspace = true } diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index 3668a2a09..cbc430529 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -7,19 +7,19 @@ edition = "2021" [dependencies] # env -dotenv = "0.15.0" +dotenv = { workspace = true } # Https server framework -actix = "0.13.0" -actix-web = "4.0.0" +actix = { workspace = true } +actix-web = { workspace = true } # To serialize and deserialize objects from json -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} +serde = { workspace = true } +serde_json = { workspace = true } # For logging and debugging -log = "^0.4" +log = { workspace = true } # date and time -chrono = { version = "0.4", features = ["serde"] } +chrono = { workspace = true } # ORM service-utils = { path = "../service-utils" } -reqwest = {version = "0.9.11"} +reqwest = { workspace = true } experimentation-platform = { path = "../experimentation-platform"} -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} +dashboard-auth = { workspace = true } diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index 570813e57..19fd4bc69 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -106,12 +106,12 @@ pub async fn diff_handler( "variantIds".to_string(), Value::String(format!("[{}]", control_id)), ); - let before = get_resolved_config(&state, &req)?; + let before = get_resolved_config(&state, &req).await?; req.insert( "variantIds".to_string(), Value::String(format!("[{}]", experimental_id)), ); - let after = get_resolved_config(&state, &req)?; + let after = get_resolved_config(&state, &req).await?; let res = DiffResponse { before, after }; return Ok(Json(res)); diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs index 5386dcccd..fe8c47f51 100644 --- a/crates/external/src/api/external_api/helpers.rs +++ b/crates/external/src/api/external_api/helpers.rs @@ -33,7 +33,7 @@ pub fn fetch_variant_id( return Err(err::InternalServerErr("".to_string())); } -pub fn get_resolved_config( +pub async fn get_resolved_config( state: &Data<AppState>, dimension_ctx: &Map<String, Value>, ) -> app::Result<Value> { @@ -45,7 +45,10 @@ pub fn get_resolved_config( .header("x-tenant", "mjos") .query(dimension_ctx) .send() - .and_then(|mut resp| resp.json()) + .await + .map_err(|e| err::InternalServerErr(e.to_string()))? + .json() + .await .map_err(|e| err::InternalServerErr(e.to_string()))?; Ok(resp) } diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 74cf7feba..49f6d1b32 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -7,28 +7,28 @@ edition = "2021" [dependencies] # env -dotenv = "0.15.0" +dotenv = { workspace = true } # Https server framework -actix = "0.13.0" -actix-web = "4.0.0" +actix = { workspace = true } +actix-web = { workspace = true } futures-util = "0.3.28" # To help generate snowflake ids -rs-snowflake = "0.6.0" +rs-snowflake = { workspace = true } #ORM -diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } -env_logger = "0.8" -anyhow = "1.0.75" -rusoto_kms = "0.48.0" -rusoto_signature = "0.48.0" -bytes = "1.4.0" -rusoto_core = "0.48.0" -base64 = "0.21.2" -urlencoding = "~2.1.2" -jsonschema = "~0.17" -log = "^0.4" -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} +env_logger = { workspace = true } +anyhow = { workspace = true } strum_macros = "^0.24" strum = {version = "^0.24"} -derive_more = "^0.99" -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} +diesel = { workspace = true } +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +derive_more = { workspace = true } +dashboard-auth = { workspace = true } diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 4b241e758..ce2dd62d7 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -4,13 +4,13 @@ version = "0.3.0" edition = "2021" [dependencies] -chrono = "0.4.26" -jsonlogic = "0.5.1" -reqwest = { version = "0.11.18", features = ["json"]} -once_cell = { version = "1.18.0" } -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.97" +once_cell = { workspace = true } +chrono = { workspace = true } +jsonlogic = { workspace = true } +reqwest = { workspace = true , features = ["json"]} +serde = { workspace = true } +serde_json = { workspace = true } tokio = {version = "1.29.1", features = ["full"]} -dotenv = "0.15.0" -derive_more = "^0.99" -log = "^0.4" +dotenv = { workspace = true } +derive_more = { workspace = true } +log = { workspace = true } diff --git a/crates/superposition_client_integration_example/Cargo.toml b/crates/superposition_client_integration_example/Cargo.toml index 2eade68df..83ad91dd4 100644 --- a/crates/superposition_client_integration_example/Cargo.toml +++ b/crates/superposition_client_integration_example/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" [dependencies] superposition_client = { path = "../superposition_client" } -chrono = "0.4.26" +chrono = { workspace = true } # Https server framework -actix = "0.13.0" -actix-web = "4.0.0" -serde_json = "1.0.97" +actix = { workspace = true } +actix-web = { workspace = true } +serde_json = { workspace = true } From da2a4e3737590eb5ca2b9960473701a19d1b69f2 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Fri, 10 Nov 2023 20:41:33 +0530 Subject: [PATCH 181/352] fix: failing build due to update of schema.rs file --- makefile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/makefile b/makefile index c5286938e..e07d4c9f9 100644 --- a/makefile +++ b/makefile @@ -62,11 +62,7 @@ validate-aws-connection: validate-psql-connection: pg_isready -h $(DOCKER_DNS) -p 5432 -setup: - # NOTE: `make migration` is being used to run the migrations for cac and experimentation in isolation, - # otherwise the tables and types of cac and experimentation spill into each others schema.rs - # NOTE: The container spinned up are stopped and removed after the work is done. - make migration +ci-setup: docker-compose up -d postgres localstack cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -81,6 +77,13 @@ setup: make legacy_db_setup echo setup completed successfully!!! +setup: + # NOTE: `make migration` is being used to run the migrations for cac and experimentation in isolation, + # otherwise the tables and types of cac and experimentation spill into each others schema.rs + # NOTE: The container spinned up are stopped and removed after the work is done. + make migration + make ci-setup + kill: -pkill -f target/debug/context-aware-config & @@ -106,7 +109,7 @@ ci-test: -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") npm ci --loglevel=error cargo test - make setup + make ci-setup make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & while ! grep -q "starting in Actix" test_logs; \ do echo "ci-test: waiting for bootup..." && sleep 4; \ From 3afb5679eb5107ec310980aec4ea7b00f21fdf5e Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Sat, 11 Nov 2023 05:00:01 +0000 Subject: [PATCH 182/352] chore(version): v0.15.0 [skip ci] --- CHANGELOG.md | 19 +++++++++++++++++++ Cargo.lock | 12 ++++++------ crates/cac_client/CHANGELOG.md | 8 ++++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 9 +++++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 8 ++++++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/external/CHANGELOG.md | 8 ++++++++ crates/external/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 8 ++++++++ crates/service-utils/Cargo.toml | 2 +- crates/superposition_client/CHANGELOG.md | 8 ++++++++ crates/superposition_client/Cargo.toml | 2 +- 14 files changed, 80 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a46c8b0..2d85f584a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.15.0 - 2023-11-11 +### Package updates +- context-aware-config bumped to context-aware-config-v0.12.0 +- external bumped to external-v0.3.0 +- cac_client bumped to cac_client-v0.4.0 +- service-utils bumped to service-utils-v0.9.0 +- superposition_client bumped to superposition_client-v0.4.0 +- experimentation-platform bumped to experimentation-platform-v0.8.0 +### Global changes +#### Bug Fixes +- failing build due to update of schema.rs file - (131463b) - Shubhranshu Sanjeev +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## v0.14.1 - 2023-11-09 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.7.1 diff --git a/Cargo.lock b/Cargo.lock index 7b1e81370..77ed6fba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,7 +616,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix-web", "chrono", @@ -857,7 +857,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.11.0" +version = "0.12.0" dependencies = [ "actix", "actix-cors", @@ -1351,7 +1351,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.7.1" +version = "0.8.0" dependencies = [ "actix", "actix-web", @@ -1373,7 +1373,7 @@ dependencies = [ [[package]] name = "external" -version = "0.2.0" +version = "0.3.0" dependencies = [ "actix", "actix-web", @@ -3473,7 +3473,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.8.0" +version = "0.9.0" dependencies = [ "actix", "actix-web", @@ -3632,7 +3632,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.3.0" +version = "0.4.0" dependencies = [ "chrono", "derive_more", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 0fa6ae5b0..52e59fb4e 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.4.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## cac_client-v0.3.0 - 2023-10-27 #### Features - multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index b0e3e0119..9b53c14f3 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 9d19ade37..b41d716ba 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.12.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## context-aware-config-v0.11.0 - 2023-11-08 #### Bug Fixes - make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 28b007fb4..a5c89bac0 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.11.0" +version = "0.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 02b6c9e15..427d03fa8 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.8.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.7.1 - 2023-11-09 #### Bug Fixes - Removing acceptance of override_keys in experiment create/update - (033597e) - ankit.mahato diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index d76800e15..1629397a5 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.7.1" +version = "0.8.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/external/CHANGELOG.md b/crates/external/CHANGELOG.md index 81055286f..48fddf2eb 100644 --- a/crates/external/CHANGELOG.md +++ b/crates/external/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## external-v0.3.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## external-v0.2.0 - 2023-11-08 #### Features - [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index cbc430529..6a654050e 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "external" -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index ef97406ab..48b58716b 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.9.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## service-utils-v0.8.0 - 2023-11-08 #### Bug Fixes - make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 49f6d1b32..90744415f 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.8.0" +version = "0.9.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index 77861b2c0..77dc81b12 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.4.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + ## superposition_client-v0.3.0 - 2023-10-27 #### Features - multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index ce2dd62d7..443da4114 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.3.0" +version = "0.4.0" edition = "2021" [dependencies] From 7730c413c78257f07d48877d41e2d677c4d574b8 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Tue, 14 Nov 2023 20:16:34 +0530 Subject: [PATCH 183/352] fix: add different auth types for exp requests to CAC --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- .../src/api/experiments/handlers.rs | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77ed6fba2..913819dcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1088,8 +1088,8 @@ dependencies = [ [[package]] name = "dashboard-auth" -version = "0.4.0" -source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.0#22a9cadea33e89aea1f9b576a6d7b3a7226b424a" +version = "0.4.1" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.1#bef8dff745d84cf73d7057c9dd3cd667f73cb4b2" dependencies = [ "actix", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index e8447bbca..7db796ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,4 +46,4 @@ rand = "0.8.5" once_cell = { version = "1.18.0" } anyhow = { version = "1.0", default-features = false } # juspay dependencies -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.0"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 078e0528c..4e48aedfa 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -154,7 +154,10 @@ async fn create( let created_contexts: Vec<ContextPutResp> = http_client .put(&url) - .header("Authorization", format!("Bearer {}", user.token)) + .header( + "Authorization", + format!("{} {}", user.auth_type, user.token), + ) .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() @@ -305,7 +308,10 @@ pub async fn conclude( let url = state.cac_host.clone() + "/context/bulk-operations"; let response = http_client .put(&url) - .header("Authorization", format!("Bearer {}", user.token)) + .header( + "Authorization", + format!("{} {}", user.auth_type, user.token), + ) .header("x-tenant", tenant.as_str()) .json(&operations) .send() @@ -635,7 +641,10 @@ async fn update_overrides( let created_contexts: Vec<ContextPutResp> = http_client .put(&url) - .header("Authorization", format!("Bearer {}", user.token)) + .header( + "Authorization", + format!("{} {}", user.auth_type, user.token), + ) .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() From 9cac54193ed47e781d4443d96242421a3a1a4247 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 16 Nov 2023 07:18:01 +0000 Subject: [PATCH 184/352] chore(version): v0.15.1 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d85f584a..70157f6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.15.1 - 2023-11-16 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.8.1 +### Global changes +#### Bug Fixes +- add different auth types for exp requests to CAC - (bd8ae88) - Kartik Gajendra + +- - - + ## v0.15.0 - 2023-11-11 ### Package updates - context-aware-config bumped to context-aware-config-v0.12.0 diff --git a/Cargo.lock b/Cargo.lock index 913819dcf..2dbe7c00e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,7 +1351,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.8.0" +version = "0.8.1" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 427d03fa8..7ca1f57a5 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.8.1 - 2023-11-16 +#### Bug Fixes +- add different auth types for exp requests to CAC - (bd8ae88) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.8.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 1629397a5..e35511536 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.8.0" +version = "0.8.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From c13b1403f8b5e97d7066c55e51b4671cbf73e94a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Thu, 26 Oct 2023 17:24:52 +0530 Subject: [PATCH 185/352] feat: update default keys --- .../src/api/default_config/handlers.rs | 93 +++++--- .../src/api/default_config/types.rs | 17 +- crates/context-aware-config/src/helpers.rs | 65 ++++-- .../Add default-config key/event.test.js | 2 +- scripts/regex.hs | 204 ++++++++++++++++++ scripts/update_schema.js | 170 +++++++++++++++ 6 files changed, 501 insertions(+), 50 deletions(-) create mode 100644 scripts/regex.hs create mode 100644 scripts/update_schema.js diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 120d78e0a..a8d2ba27f 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,18 +1,22 @@ use super::types::CreateReq; use crate::{ - db::{models::DefaultConfig, schema::default_configs::dsl::default_configs}, + db::{self, models::DefaultConfig, schema::default_configs::dsl::default_configs}, helpers::validate_jsonschema, }; use actix_web::{ + error::{ErrorBadRequest, ErrorInternalServerError}, put, web::{self, Data}, HttpResponse, Scope, }; use chrono::Utc; use dashboard_auth::types::User; -use diesel::RunQueryDsl; -use jsonschema::{Draft, JSONSchema}; -use serde_json::Value; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Value}; use service_utils::service::types::{AppState, DbConnection}; pub fn endpoints() -> Scope { @@ -26,13 +30,34 @@ async fn create( request: web::Json<CreateReq>, user: User, db_conn: DbConnection, -) -> HttpResponse { +) -> actix_web::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - let schema = Value::Object(req.schema); + let key = key.into_inner(); + + let (value, schema) = match (req.value, req.schema) { + (Some(val), Some(schema)) => (val, Value::Object(schema)), + (Some(val), None) => { + let (_, schema) = fetch_default_key(&key, &mut conn) + .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; + (val, schema) + } + (None, Some(schema)) => { + let (value, _) = fetch_default_key(&key, &mut conn) + .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; + (value, Value::Object(schema)) + } + (None, None) => { + log::info!("value/schema not provided."); + return Err(ErrorBadRequest( + json!({"message": "Either value/schema required."}), + )); + } + }; + if let Err(e) = validate_jsonschema(&state.default_config_validation_schema, &schema) { - return HttpResponse::BadRequest().body(e); + return Err(ErrorBadRequest(json!({ "message": e }))); }; let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) @@ -41,39 +66,59 @@ async fn create( Ok(jschema) => jschema, Err(e) => { log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); - return HttpResponse::BadRequest().body("Bad json schema."); + return Err(ErrorBadRequest(json!({"message": "Bad json schema."}))); } }; - match jschema.validate(&req.value) { - Ok(_) => (), - Err(_) => { - log::info!("Validation for value with given JSON schema failed."); - return HttpResponse::BadRequest() - .body("Validation with given schema failed."); - } - }; + if let Err(e) = jschema.validate(&value) { + let verrors = e.collect::<Vec<ValidationError>>(); + log::info!( + "Validation for value with given JSON schema failed: {:?}", + verrors + ); + return Err(ErrorBadRequest( + json!({"message": "Validation with given schema failed."}), + )); + } let new_default_config = DefaultConfig { - key: key.into_inner(), - value: req.value, - schema: schema, + key, + value, + schema, created_by: user.email, created_at: Utc::now(), }; let upsert = diesel::insert_into(default_configs) .values(&new_default_config) + .on_conflict(db::schema::default_configs::key) + .do_update() + .set(&new_default_config) .execute(&mut conn); match upsert { - Ok(_) => { - return HttpResponse::Created().body("DefaultConfig created successfully.") - } + Ok(_) => Ok(HttpResponse::Ok().json(json!({ + "message": "DefaultConfig created/updated successfully." + }))), Err(e) => { log::info!("DefaultConfig creation failed with error: {e}"); - return HttpResponse::InternalServerError() - .body("Failed to create DefaultConfig"); + Err(ErrorInternalServerError( + json!({"message": "Failed to create DefaultConfig"}), + )) } } } + +fn fetch_default_key( + key: &String, + conn: &mut PooledConnection<ConnectionManager<PgConnection>>, +) -> anyhow::Result<(Value, Value)> { + let res: (Value, Value) = default_configs + .filter(db::schema::default_configs::key.eq(key)) + .select(( + db::schema::default_configs::value, + db::schema::default_configs::schema, + )) + .get_result::<(Value, Value)>(conn)?; + Ok(res) +} diff --git a/crates/context-aware-config/src/api/default_config/types.rs b/crates/context-aware-config/src/api/default_config/types.rs index 9bc4826a8..559e41968 100644 --- a/crates/context-aware-config/src/api/default_config/types.rs +++ b/crates/context-aware-config/src/api/default_config/types.rs @@ -1,8 +1,17 @@ -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use serde_json::{Map, Value}; -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct CreateReq { - pub value: Value, - pub schema: Map<String, Value>, + #[serde(default, deserialize_with = "deserialize_option")] + pub value: Option<Value>, + pub schema: Option<Map<String, Value>>, +} + +fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(Some(value)) } diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index 2d006d518..23531c814 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -4,36 +4,59 @@ use serde_json::{json, Value}; use std::collections::HashMap; pub fn get_default_config_validation_schema() -> JSONSchema { - let my_schema = json!({ + let my_schema = json!( + { "type": "object", "properties": { - "type": { - "enum": ["string", "object", "enum", "number", "boolean", "array"] - } + "type": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + } + ] + } }, - "required": ["type"], + "required": [ + "type" + ], "allOf": [ - // { - // "if": { - // "properties": { "type": { "const": "object" } } - // }, - // "then": { - // "properties": { "properties": { "type": "object" } }, - // "required": ["properties"] - // } - // }, - { + { "if": { - "properties": { "type": { "const": "string" } } + "properties": { + "type": { + "const": "string" + } + } }, "then": { - "properties": { "pattern": { "type": "string" } }, - "required": ["pattern"] + "properties": { + "pattern": { + "type": "string" + } + }, + "required": [ + "pattern" + ] } - }, - // TODO: Add validations for Array types. + } + // TODO: Add validations for Array types. ] - }); + }); JSONSchema::options() .with_draft(Draft::Draft7) diff --git a/postman/cac/Default Config/Add default-config key/event.test.js b/postman/cac/Default Config/Add default-config key/event.test.js index 0a23eb523..8ec09bb5c 100644 --- a/postman/cac/Default Config/Add default-config key/event.test.js +++ b/postman/cac/Default Config/Add default-config key/event.test.js @@ -24,7 +24,7 @@ function getConfigAndTest(key, value) { } pm.test("201 check", function () { - pm.response.to.have.status(201); + pm.response.to.have.status(200); }) pm.test("Check if key added to default config", function () { diff --git a/scripts/regex.hs b/scripts/regex.hs new file mode 100644 index 000000000..b95569c0c --- /dev/null +++ b/scripts/regex.hs @@ -0,0 +1,204 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE MultiWayIf #-} +{-# OPTIONS_GHC -Wno-name-shadowing #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-unused-imports #-} + +import System.Environment +import qualified Data.Text as T +import GHC.Generics +import qualified GHC.Base as G +import Data.Aeson +import Data.Maybe + + + +data Platform + = ANDROID + | IOS + | WEB + deriving (Generic, Show, Eq, Ord, Enum, Read) + +instance FromJSON Platform + where + parseJSON (String a) = genericParseJSON defaultOptions $ String (T.toUpper a) + parseJSON _ = fail "Invalid Platform" + +instance ToJSON Platform where + toJSON = String . T.toLower . T.pack . show + +createPathRegex :: T.Text -> T.Text -> Platform -> T.Text +createPathRegex "in.juspay.godel.placeholder" asset platform = createPathRegex "in.juspay.godel" asset platform +createPathRegex app asset platform = createPathRegex' $ + case asset of + "index" -> case platform of + WEB -> [stdIndexPathCmn, newIndexPathCmn] + _ -> [stdIndexPathApp, stdIndexPathCmn, newIndexPathCmn] + + "manifest" -> (: [stdManifestPath]) $ case platform of + WEB -> ["hyper", "bundles", "web", "release"] + IOS -> ["juspay", "payments", "release", "IOS"] + ANDROID -> ["juspay", "payments", "release"] + + "fonts" -> [[".*"]] + + "sdk_config" -> [[".*"]] + + "tracker" -> (: [["hyper", "bundles", "app", "tracker", semVerRegex]]) $ case platform of + WEB -> ["hyper", "bundles", "web", "release", createMappRegex "in.juspay.hyperos", sdkV2Regex', clientIdRegex, "(stable|" <> assetVerRegex <> "|staggered)"] + _ -> ["juspay", "payments", "2\\.0", "release(\\/" <> assetVerRegex <> ")?"] + + "config" -> case (app, platform) of + ("in.juspay.godel", _) -> [[".*"]] + ("in.juspay.hyperos", WEB) -> [[".*"]] + ("in.juspay.hyperos", _) -> [["juspay", "payments", "2\\.0", "release(\\/"<> assetVerRegex <> ")?"]] + (_, WEB) -> [stdAssetPathCmn, newConfigPathCmn] + _ -> [stdAssetPathCmn, stdAssetPathApp, newConfigPathCmn] + + _ -> case (app, platform) of + ("in.juspay.godel", _) -> [[".*"]] + (_, WEB) -> [stdAssetPathCmn, galactusAssetPath, galactusAssetPathV1, galactusAssetPathV1', newConfigPathCmn] + ("in.yatri.consumer",_) -> [preReleaseAssetPath, galactusAssetPathV1'] + ("in.yatri.provider",_) -> [preReleaseAssetPath, galactusAssetPathV1'] + _ -> [stdAssetPathCmn, stdAssetPathApp, galactusAssetPath, galactusAssetPathV1, galactusAssetPathV1', newConfigPathCmn] + + where + createPathRegex' :: [[T.Text]] -> T.Text + createPathRegex' patterns = + if asset /= "index" && app == "in.juspay.escrow" + then (createPathRegex'' patterns) <> "|" <> (createPathRegex "in.juspay.hyperpay" asset platform) + else createPathRegex'' patterns + + createPathRegex'' :: [[T.Text]] -> T.Text + createPathRegex'' = T.intercalate "|" . G.map ((bucketPath <>) . T.intercalate "\\/" . (++ [fileName <> "$"])) + + bucketPath = + case app of + "in.juspay.godel" -> "^https:\\/\\/.*\\/" + _ -> "^https:\\/\\/" <> (T.replace "." "\\." getReadAssetsDomain) <> "\\/" + + stdManifestPath = ["hyper", "bundles", "in\\.juspay\\.merchants", clientIdRegex, platform', "(cug|release)"] + + platform' = T.toLower . T.pack $ show platform + + ext = "\\." <> (getExtensionForAsset platform app asset) + + lookupAppFName "com.juspay.gemi" = \case + "strings" -> Just $ "strings(_[a-z,A-Z,0-9]+)?\\." <> ext + fname -> Just $ fname <> "\\." <> ext + lookupAppFName _ = const Nothing + + platformPrefix = case platform of + WEB -> "" + _ -> "v1-" + + getDefaultFName fname = platformPrefix <> fname <> ext + + fileName = + case asset of + "manifest" -> "manifest" <> ext + "sdk_config" -> "sdk_config" <> ext + "fonts" -> "(.*)" <> ext + "certificates" -> "certificates_v1" <> ext + "acs_js_source" -> getDefaultFName "acs" + "boot_loader_js_source" -> getDefaultFName "boot_loader" + "index" -> case platform of + WEB -> getDefaultFName "(prod-split_)?(br[0-9]+-)?index[0-9]*" + _ -> getDefaultFName "index_bundle" + _ -> let mbAppFName = lookupAppFName app asset + in fromMaybe (getDefaultFName asset) mbAppFName + + clientIdRegex = "[a-z,0-9]+((\\.|,|-|_)[a-z,0-9]+)*" + branchRegex = "[^\\/]+" + + scope = + case platform of + IOS -> "release\\/IOS" + _ -> "release" + + sdkV1Regex = "1\\.0rc1" + sdkV2Regex = "2\\.0rc1" + sdkV2Regex' = "2\\.0\\.0" + assetVerRegex = "([0-9].){2}[0-9]+" + semVerRegex = "([0-9]+\\.){2}[0-9]+" + calVerRegex = semVerRegex <> "-(release|hotfix)(-[a-z0-9]+)?-[0-9]+(-[0-9]+)?\\.[0-9]+" + preReleaseVerRegex = semVerRegex <> "-(main)(-"<>clientIdRegex <>")?\\.[0-9]+" + + mappRegex = createMappRegex app + + createMappRegex = (<> ("(\\." <> clientIdRegex <> ")?")) . T.replace "." "\\." + + indexV1Regex = sdkV1Regex <> "_[0-9]+" + + indexV2Regex = sdkV2Regex <> "_[0-9]+" + + newIndexPathPrefix = ["hyper", "bundles", "app", mappRegex] + newIndexPathCmn = newIndexPathPrefix <> case app of + "com.juspay.gemi" -> [branchRegex, clientIdRegex, platform'] + _ -> [versionRegex, platform'] + where + versionList = [indexV1Regex, indexV2Regex, semVerRegex, calVerRegex] + versionRegex = "(" <> (T.intercalate "|" versionList) <> ")" + + stdIndexPathCmn = + case app of + "in.juspay.hypercredit" -> stdIndexPathCmn' indexV1Regex + _ -> stdIndexPathCmn' indexV2Regex + where + stdIndexPathCmn' indexVRegex = ["hyper", "bundles", platform', "release", mappRegex, sdkV2Regex', clientIdRegex, "(stable|" <> indexVRegex <> "|staggered)"] + stdIndexPathApp = + case app of + "in.juspay.arya" -> ["juspay", mappRegex, scope, sdkV1Regex, indexV1Regex] + "in.juspay.hypercredit" -> stdIndexPathApp' indexV1Regex + "neopenkochi.yatri" -> becknPath + "neopenkochi.yatripartner" -> becknPath + _ -> stdIndexPathApp' indexV2Regex + where + becknPath = ["juspay", "beckn", mappRegex, scope, sdkV1Regex, indexV1Regex] + stdIndexPathApp' indexVRegex = ["juspay", "payments", mappRegex, scope, sdkV2Regex <> "(\\/(" <> indexVRegex <> "|" <> "staggered-release-assets))?"] + + stdAssetPathCmn = ["hyper", "bundles", platform', "release", mappRegex, sdkV2Regex', clientIdRegex, "(stable|" <> assetVerRegex <> "|staggered)"] + + newConfigPathCmn = ["hyper", "bundles", "config", mappRegex, clientIdRegex, versionRegex] + where + versionRegex = "(" <> (T.intercalate "|" [calVerRegex, indexV2Regex, indexV1Regex, semVerRegex]) <> ")" + + stdAssetPathApp = ["juspay", "payments", mappRegex, "release" <> (if platform == IOS then "(\\/IOS)?" else "") <> "(\\/(" <> indexV2Regex <> "|" <> assetVerRegex <> "|" <> "staggered-release-assets))?"] + + galactusAssetPath = ["hyper", "bundles", createMappRegex "in.juspay.merchants", clientIdRegex, "configuration", "[0-9]+\\.[0-9]+"] + galactusAssetPathV1' = ["hyper", "configs", clientIdRegex, mappRegex, semVerRegex] + preReleaseAssetPath = ["hyper", "configs", clientIdRegex, mappRegex, preReleaseVerRegex] + + galactusAssetPathV1 = ["hyper", "configs", clientIdRegex, mappRegex, semVerRegex, "configuration"] + + getReadAssetsDomain :: T.Text + getReadAssetsDomain = T.pack "assets.juspay.in" + + getExtensionForAsset :: Platform -> T.Text -> T.Text -> T.Text + getExtensionForAsset platform app asset = + case (app, asset) of + ("com.juspay.gemi", asset) | asset /= "index" -> "json" + (_ , "sdk_config") -> "json" + (_ , "manifest") -> "json" + (_ , "fonts") -> "ttf" + _ -> getDefaultFNameExt platform + + getDefaultFNameExt :: Platform -> T.Text + getDefaultFNameExt = + \case + WEB -> "js" + IOS -> "jsa" + ANDROID -> "zip" + + +main :: IO () +main = do + args <- getArgs + case args of + [arg1, arg2] -> do + let android = createPathRegex (T.pack arg1) (T.pack arg2) ANDROID + let ios = createPathRegex (T.pack arg1) (T.pack arg2) IOS + let web = createPathRegex (T.pack arg1) (T.pack arg2) WEB + putStrLn (show (android <> (T.pack "|") <> ios <> (T.pack "|") <> web)) + _ -> error "Provide proper Haskell <argument>" \ No newline at end of file diff --git a/scripts/update_schema.js b/scripts/update_schema.js new file mode 100644 index 000000000..a034587ce --- /dev/null +++ b/scripts/update_schema.js @@ -0,0 +1,170 @@ +import fetch from "node-fetch"; +import cp from "child_process"; + +let host = "http://localhost:8080" // change it respective host + +// Api calls +// Using a local api to get the app_name and asset from the key_name (getApp) + +async function getConfig() { + try { + const response = await fetch(`${host}/config`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + }); + let config = await response.json(); + switch(response.status) { + case 200: + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function getApp(key, val) { + try { + let body = {"key": key, "val": val}; + const response = await fetch(`http://localhost:8080/config/getapp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + body: JSON.stringify(body), + }); + switch(response.status) { + case 200: + let config = await response.json(); + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function updateRegex(key, isArray, pattern) { + try { + let body = {}; + + if (isArray) { + body = {"schema":{"type": ["null", "array"],"items": {"type": "string", "pattern": pattern}}}; + } else { + body = {"schema":{"type": ["null", "string"], "pattern": pattern}}; + } + + const response = await fetch(`${host}/default-config/${key}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + body: JSON.stringify(body), + }); + switch(response.status) { + case 200: + let config = await response.text(); + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function getRegex(app, asset) { + try { + let resp = cp.execSync(`runghc regex.hs ${app} ${asset}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); + return resp; + } catch (error) { + console.error(error); + throw error; + } +} + +async function getRegexForValue(key, value) { + let regex = ""; + console.log(key); + if (typeof value !== "string") { + regex = "^(default_array_ignore_this|in.juspay.dotp|in.juspay.ec|in.juspay.escrow|in.juspay.flyer|in.juspay.hyperos|in.juspay.hyperos.placeholder|in.juspay.hyperpay|in.juspay.upiintent|in.juspay.arya|in.juspay.hypercredit|in.juspay.godel|in.juspay.godel.placeholder|in.juspay.hyperapi|in.juspay.hyperupi|in.juspay.inappupi|in.juspay.vies|net.openkochi.yatri|net.openkochi.yatripartner|in.juspay.hyperpay.placeholder)$"; + let update_response = await updateRegex(key, true, regex); + console.log(update_response); // + + return Promise.resolve(update_response); + } else { + if (key.endsWith("etag")) { + regex = "^default_str_ignore_this$|^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]+$"; + } else { + try { + let resp = await getApp(key, value); + let regex_from_galactus = await getRegex(resp.app_name, resp.asset); + + let add_default_regex = "^default_str_ignore_this$|" + JSON.parse(regex_from_galactus); + regex = add_default_regex; + + } catch (error) { + console.error(error); + throw error; + } + } + console.log(regex); + let update_response = await updateRegex(key, false, regex); + console.log(update_response); // + + return Promise.resolve(update_response); + } + +} + +async function decideRegex(i, default_configs) { + if (i >= Object.keys(default_configs).length){ + return Promise.resolve("done"); + } + let key = Object.keys(default_configs)[i]; + let value = Object.values(default_configs)[i]; + + try { + let regex_response = await getRegexForValue(key, value); + if (regex_response) { + return await decideRegex(i+1, default_configs); + } else { + return Promise.reject("notDone"); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function heartbeat(){ + let config = await getConfig(); + let default_configs = config.default_configs; + + let keysToFilter = ["pmTestKey1972", "pmTestKey1999"]; + keysToFilter.forEach(key => { + delete default_configs[key]; + }); + + try { + await decideRegex(0, default_configs); + } catch (error) { + console.error(error); + throw error; + } +} + +heartbeat(); From c33b445f91375347208bd049a1830b5df625a3b6 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 16 Nov 2023 17:42:52 +0530 Subject: [PATCH 186/352] fix: fixed deployment ConfigNotFound failure --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5918fd64b..996e8eeb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ ARG CONTEXT_AWARE_CONFIG_VERSION RUN apt-get update && apt-get install -y libpq5 ca-certificates COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config +COPY --from=builder /build/Cargo.toml /app/Cargo.toml ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT -CMD ["/app/context-aware-config"] +CMD ["/app/context-aware-config"] \ No newline at end of file From 3d037dc9682d396d28b4e4a2056de6ffa104356b Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 16 Nov 2023 13:45:05 +0000 Subject: [PATCH 187/352] chore(version): v0.16.0 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70157f6c9..f61dda336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.16.0 - 2023-11-16 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.0 +### Global changes +#### Bug Fixes +- fixed deployment ConfigNotFound failure - (be381f1) - Shubhranshu Sanjeev +#### Features +- update default keys - (d6b9992) - ankit.mahato + +- - - + ## v0.15.1 - 2023-11-16 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.8.1 diff --git a/Cargo.lock b/Cargo.lock index 2dbe7c00e..9b3374497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,7 +857,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.12.0" +version = "0.13.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index b41d716ba..f0784a03c 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.13.0 - 2023-11-16 +#### Features +- update default keys - (d6b9992) - ankit.mahato + +- - - + ## context-aware-config-v0.12.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index a5c89bac0..957296912 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.12.0" +version = "0.13.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 78a03e29a36924b783d713c84fd589746ef5bb5b Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Wed, 8 Nov 2023 18:15:17 +0530 Subject: [PATCH 188/352] fix: sort json while context creation --- Cargo.lock | 38 ++++----- crates/context-aware-config/Cargo.toml | 1 + .../src/api/context/handlers.rs | 21 ++--- crates/context-aware-config/src/helpers.rs | 81 +++++++++++++++++++ scripts/context_id_update.py | 35 ++++++++ 5 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 scripts/context_id_update.py diff --git a/Cargo.lock b/Cargo.lock index 9b3374497..90e4d06c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,6 +881,7 @@ dependencies = [ "frontend", "futures", "futures-util", + "itertools", "json-patch", "jsonschema", "leptos", @@ -1805,9 +1806,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -3039,17 +3040,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" dependencies = [ "cc", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -3213,9 +3213,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", "ring", @@ -3225,18 +3225,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.2", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", "untrusted", @@ -3295,9 +3295,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -3595,9 +3595,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" @@ -4110,9 +4110,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 957296912..6d16aaf2a 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -52,6 +52,7 @@ tracing-log = "0.1.3" tracing = { version = "0.1.37", features = ["valuable"]} tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"]} valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" futures = "0.3.28" actix-http = "3.3.1" futures-util = "0.3.28" diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 9530f3653..af81c0202 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use crate::helpers::json_to_sorted_string; use crate::{ api::{ context::types::{ @@ -33,8 +32,9 @@ use diesel::{ Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, }; use jsonschema::{Draft, JSONSchema, ValidationError}; -use serde_json::{from_value, json, Map, Value, Value::Null}; +use serde_json::{from_value, json, Map, Value}; use service_utils::{helpers::ToActixErr, service::types::DbConnection}; +use std::collections::HashMap; pub fn endpoints() -> Scope { Scope::new("") @@ -181,8 +181,8 @@ fn create_ctx_from_put_req( } Ok(p) => p, }; - let context_id = generate_context_id(&ctx_condition); - let override_id = blake3::hash(ctx_override.to_string().as_bytes()).to_string(); + let context_id = hash(&ctx_condition); + let override_id = hash(&ctx_override); Ok(Context { id: context_id.clone(), value: ctx_condition, @@ -194,8 +194,9 @@ fn create_ctx_from_put_req( }) } -fn generate_context_id(ctx_condition: &Value) -> String { - blake3::hash(ctx_condition.to_string().as_bytes()).to_string() +fn hash(val: &Value) -> String { + let sorted_str: String = json_to_sorted_string(val); + blake3::hash(sorted_str.as_bytes()).to_string() } fn update_override_of_existing_ctx( @@ -208,7 +209,7 @@ fn update_override_of_existing_ctx( .select(dsl::override_) .first(conn)?; json_patch::merge(&mut new_override, &ctx.override_); - let new_override_id = blake3::hash((new_override).to_string().as_bytes()).to_string(); + let new_override_id = hash(&new_override); let new_ctx = Context { override_: new_override, override_id: new_override_id, @@ -283,7 +284,7 @@ fn r#move( use contexts::dsl; let req = req.into_inner(); let ctx_condition = Value::Object(req.context); - let new_ctx_id = generate_context_id(&ctx_condition); + let new_ctx_id = hash(&ctx_condition); let dimension_schema_map = get_all_dimension_schema_map(conn)?; let priority = match validate_dimensions_and_calculate_priority( &ctx_condition, @@ -426,7 +427,7 @@ async fn list_contexts( .limit(i64::from(size)) .offset(i64::from(size * (page - 1))) .load(&mut conn) - .map_err_to_internal_server("Failed to execute query, error", Null)?; + .map_err_to_internal_server("Failed to execute query, error", Value::Null)?; Ok(web::Json(result)) } diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index 23531c814..ef4674ebe 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -1,4 +1,5 @@ use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; +use itertools::{self, Itertools}; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -147,6 +148,34 @@ pub fn validate_jsonschema( res } +pub fn json_to_sorted_string(v: &Value) -> String { + match v { + Value::Object(m) => { + let mut new_str: String = String::from(""); + for (i, val) in m.iter().sorted_by_key(|item| item.0) { + let p: String = json_to_sorted_string(val); + new_str.push_str(i); + new_str.push_str(&String::from(":")); + new_str.push_str(&p); + new_str.push_str(&String::from("$")); + } + new_str + } + Value::String(m) => m.to_string(), + Value::Number(m) => m.to_string(), + Value::Bool(m) => m.to_string(), + Value::Null => String::from("null"), + Value::Array(m) => { + let mut new_vec: Vec<String> = m + .iter() + .map(|item| json_to_sorted_string(item)) + .collect::<Vec<String>>(); + new_vec.sort(); + new_vec.join(",") + } + } +} + // ************ Tests ************* #[cfg(test)] @@ -198,4 +227,56 @@ mod tests { assert_eq!(ok_string_validation, Ok(())); assert_eq!(error_string_validation, true); } + + #[test] + fn test_json_to_sorted_string() { + let first_condition: Value = json!({ + "and": [ + { + "==": [ + { + "var": "os" + }, + "android" + ] + }, + { + "==": [ + { + "var": "clientId" + }, + "geddit" + ] + } + ] + }); + + let second_condition: Value = json!({ + "and": [ + { + "==": [ + { + "var": "clientId" + }, + "geddit" + ] + }, + { + "==": [ + { + "var": "os" + }, + "android" + ] + } + ] + }); + let expected_string: String = + "and:==:android,var:os$$,==:geddit,var:clientId$$$".to_owned(); + assert_eq!(json_to_sorted_string(&first_condition), expected_string); + assert_eq!( + json_to_sorted_string(&first_condition), + json_to_sorted_string(&second_condition) + ); + } } diff --git a/scripts/context_id_update.py b/scripts/context_id_update.py new file mode 100644 index 000000000..1ecaf8ccf --- /dev/null +++ b/scripts/context_id_update.py @@ -0,0 +1,35 @@ +import requests +import json +import os + + +context_url = os.getenv("CONTEXT_URL","http://localhost:8080/context") +config_url = os.getenv("CONFIG_URL","http://localhost:8080/config") +cac_config = requests.get(url=config_url, headers={"x-tenant": "mjos"}).json() + +print(cac_config) + +for i in cac_config["contexts"]: + context_id = i["id"] + move_context_data = { + "context": i["condition"] + } + + update_context_data = { + "context": i["condition"], + "override": cac_config["overrides"][i["override_with_keys"][0]] + } + move_context = requests.put(url=context_url + "/move/" + str(context_id), json=move_context_data, headers={"x-tenant": "mjos", "Authorisation": "12345678"}) + if move_context.status_code == 200: + print("move context request success\n") + print(move_context.json()) + else: + print("move context request failed\n") + print(json.dumps(move_context_data)) + update_context = requests.put(url=context_url, json=update_context_data, headers={"x-tenant": "mjos", "Authorisation": "12345678"}) + if update_context.status_code == 200: + print("update context request success\n") + print(update_context.json()) + else: + print("update context request failed\n") + print(json.dumps(update_context_data)) From ba87783b13ee44942f44d7361a2f0d9cbfc86bb2 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 22 Nov 2023 06:02:00 +0000 Subject: [PATCH 189/352] chore(version): v0.16.1 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f61dda336..6dfdd174f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.16.1 - 2023-11-22 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.1 +### Global changes +#### Bug Fixes +- PICAF-25066 sort json while context creation - (3bd7a97) - Pratik Mishra + +- - - + ## v0.16.0 - 2023-11-16 ### Package updates - context-aware-config bumped to context-aware-config-v0.13.0 diff --git a/Cargo.lock b/Cargo.lock index 90e4d06c9..0cc5335e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,7 +857,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.13.0" +version = "0.13.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index f0784a03c..281aa3aea 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.13.1 - 2023-11-22 +#### Bug Fixes +- PICAF-25066 sort json while context creation - (3bd7a97) - Pratik Mishra + +- - - + ## context-aware-config-v0.13.0 - 2023-11-16 #### Features - update default keys - (d6b9992) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 6d16aaf2a..4f8bcd41e 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.13.0" +version = "0.13.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 892f29c02b6406be781dc3059b5a7fa5e4077477 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Wed, 29 Nov 2023 18:09:32 +0530 Subject: [PATCH 190/352] fix: allow ramp 0 --- Cargo.lock | 37 ++++++++++--------- .../src/api/experiments/handlers.rs | 4 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cc5335e2..079c945a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1806,9 +1806,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", "http", @@ -3040,16 +3040,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.3" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", - "getrandom", "libc", + "once_cell", "spin", "untrusted", - "windows-sys 0.48.0", + "web-sys", + "winapi", ] [[package]] @@ -3213,9 +3214,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", @@ -3225,18 +3226,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ "base64 0.21.2", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -3295,9 +3296,9 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -3595,9 +3596,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "strsim" @@ -4110,9 +4111,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" -version = "0.9.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 4e48aedfa..f48673785 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -460,7 +460,9 @@ async fn ramp( message: format!("The traffic_percentage cannot exceed {}", max), possible_fix: format!("Provide a traffic percentage less than {}", max), })); - } else if new_traffic_percentage == old_traffic_percentage { + } else if new_traffic_percentage != 0 + && new_traffic_percentage == old_traffic_percentage + { return Err(err::BadRequest(ErrorResponse { message: "The traffic_percentage is same as provided".to_string(), possible_fix: "".to_string(), From ce7e53e0b3becf302f493c869a1c5562031985f7 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 30 Nov 2023 06:45:54 +0000 Subject: [PATCH 191/352] chore(version): v0.16.2 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dfdd174f..95646659f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.16.2 - 2023-11-30 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.8.2 +### Global changes +#### Bug Fixes +- allow ramp 0 - (b8d49aa) - Kartik Gajendra + +- - - + ## v0.16.1 - 2023-11-22 ### Package updates - context-aware-config bumped to context-aware-config-v0.13.1 diff --git a/Cargo.lock b/Cargo.lock index 079c945a4..9029d24e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,7 +1352,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.8.1" +version = "0.8.2" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 7ca1f57a5..dd5c146b6 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.8.2 - 2023-11-30 +#### Bug Fixes +- allow ramp 0 - (b8d49aa) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.8.1 - 2023-11-16 #### Bug Fixes - add different auth types for exp requests to CAC - (bd8ae88) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index e35511536..d9927eed7 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.8.1" +version = "0.8.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From bec2f1b4cd74d84efa0c901dcfa518562fd9500a Mon Sep 17 00:00:00 2001 From: Prasanna P <prasanna.p@juspay.in> Date: Mon, 25 Sep 2023 12:30:55 +0530 Subject: [PATCH 192/352] fix: - Cac client library changes to consume backend api response --- .gitignore | 1 + libs/js/index.js | 1 + libs/js/index.ts | 39 - libs/js/package-lock.json | 10536 +++++++++++++++++++++++++++++- libs/js/package.json | 54 +- libs/js/src/index.ts | 41 + libs/js/src/types.ts | 16 + libs/js/src/utils/deepMerge.ts | 44 + libs/js/src/utils/operations.ts | 9 + libs/js/tsconfig.json | 26 +- libs/js/types.ts | 19 - libs/js/utils/deepMerge.ts | 44 - libs/js/utils/operations.ts | 15 - libs/js/webpack.config.js | 36 + 14 files changed, 10706 insertions(+), 175 deletions(-) create mode 100644 libs/js/index.js delete mode 100644 libs/js/index.ts create mode 100644 libs/js/src/index.ts create mode 100644 libs/js/src/types.ts create mode 100644 libs/js/src/utils/deepMerge.ts create mode 100644 libs/js/src/utils/operations.ts delete mode 100644 libs/js/types.ts delete mode 100644 libs/js/utils/deepMerge.ts delete mode 100644 libs/js/utils/operations.ts create mode 100644 libs/js/webpack.config.js diff --git a/.gitignore b/.gitignore index 2c34c41fb..aa9ad0258 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Node artifact files node_modules/ +dist/ # Log files *.log diff --git a/libs/js/index.js b/libs/js/index.js new file mode 100644 index 000000000..696ab0765 --- /dev/null +++ b/libs/js/index.js @@ -0,0 +1 @@ +!function(r,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["Context-Aware-Config"]=t():r["Context-Aware-Config"]=t()}(self,(function(){return r={193:function(r,t,e){"use strict";var n=this&&this.__assign||function(){return n=Object.assign||function(r){for(var t,e=1,n=arguments.length;e<n;e++)for(var i in t=arguments[e])Object.prototype.hasOwnProperty.call(t,i)&&(r[i]=t[i]);return r},n.apply(this,arguments)},i=this&&this.__createBinding||(Object.create?function(r,t,e,n){void 0===n&&(n=e);var i=Object.getOwnPropertyDescriptor(t,e);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(r,n,i)}:function(r,t,e,n){void 0===n&&(n=e),r[n]=t[e]}),o=this&&this.__setModuleDefault||(Object.create?function(r,t){Object.defineProperty(r,"default",{enumerable:!0,value:t})}:function(r,t){r.default=t}),u=this&&this.__importStar||function(r){if(r&&r.__esModule)return r;var t={};if(null!=r)for(var e in r)"default"!==e&&Object.prototype.hasOwnProperty.call(r,e)&&i(t,r,e);return o(t,r),t},a=this&&this.__spreadArray||function(r,t,e){if(e||2===arguments.length)for(var n,i=0,o=t.length;i<o;i++)!n&&i in t||(n||(n=Array.prototype.slice.call(t,0,i)),n[i]=t[i]);return r.concat(n||Array.prototype.slice.call(t))};Object.defineProperty(t,"__esModule",{value:!0}),t.CacReader=void 0;var f=u(e(962)),l=e(925),c=e(196),p=function(){function r(r){this.contexts=r.contexts,this.overrides=r.overrides,this.defaultConfig=r.default_configs}return r.prototype.evaluateConfig=function(r){for(var t=this,e=[],i=0;i<this.contexts.length;i++)f.apply(this.contexts[i].condition,r)&&e.push.apply(e,this.contexts[i].override_with_keys.map((function(r){return t.overrides[r]})));var o=n({},this.defaultConfig);return l.deepMerge.apply(void 0,a([o],e,!1))},r}();t.CacReader=p,f.add_operation(">>",c.compareSemanticIsGreater)},925:function(r,t){"use strict";var e=this&&this.__spreadArray||function(r,t,e){if(e||2===arguments.length)for(var n,i=0,o=t.length;i<o;i++)!n&&i in t||(n||(n=Array.prototype.slice.call(t,0,i)),n[i]=t[i]);return r.concat(n||Array.prototype.slice.call(t))};Object.defineProperty(t,"__esModule",{value:!0}),t.deepMerge=void 0;var n=function(r){return r===Object(r)&&!Array.isArray(r)};t.deepMerge=function(r){for(var i=[],o=1;o<arguments.length;o++)i[o-1]=arguments[o];if(!i.length)return r;var u=r;if(n(u))for(var a=i.length,f=0;f<a;f+=1){var l=i[f];if(n(l))for(var c in l)l.hasOwnProperty(c)&&(n(l[c])?(u[c]&&n(u[c])||(u[c]={}),(0,t.deepMerge)(u[c],l[c])):Array.isArray(u[c])&&Array.isArray(l[c])?u[c]=e([],l[c],!0):u[c]=l[c])}return u}},196:function(r,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.compareSemanticIsGreater=void 0;var e=function(r,t){for(var e=r.split("."),n=t.split("."),i=0;i<e.length;i++)if(e[i]>n[i])return!0;return!1};t.compareSemanticIsGreater=function(r,t,n){return void 0===n?e(r,t):e(r,t)&&e(t,n)}},962:function(r,t,e){var n,i;n=function(){"use strict";Array.isArray||(Array.isArray=function(r){return"[object Array]"===Object.prototype.toString.call(r)});var r={},t={"==":function(r,t){return r==t},"===":function(r,t){return r===t},"!=":function(r,t){return r!=t},"!==":function(r,t){return r!==t},">":function(r,t){return r>t},">=":function(r,t){return r>=t},"<":function(r,t,e){return void 0===e?r<t:r<t&&t<e},"<=":function(r,t,e){return void 0===e?r<=t:r<=t&&t<=e},"!!":function(t){return r.truthy(t)},"!":function(t){return!r.truthy(t)},"%":function(r,t){return r%t},log:function(r){return console.log(r),r},in:function(r,t){return!(!t||void 0===t.indexOf)&&-1!==t.indexOf(r)},cat:function(){return Array.prototype.join.call(arguments,"")},substr:function(r,t,e){if(e<0){var n=String(r).substr(t);return n.substr(0,n.length+e)}return String(r).substr(t,e)},"+":function(){return Array.prototype.reduce.call(arguments,(function(r,t){return parseFloat(r,10)+parseFloat(t,10)}),0)},"*":function(){return Array.prototype.reduce.call(arguments,(function(r,t){return parseFloat(r,10)*parseFloat(t,10)}))},"-":function(r,t){return void 0===t?-r:r-t},"/":function(r,t){return r/t},min:function(){return Math.min.apply(this,arguments)},max:function(){return Math.max.apply(this,arguments)},merge:function(){return Array.prototype.reduce.call(arguments,(function(r,t){return r.concat(t)}),[])},var:function(r,t){var e=void 0===t?null:t,n=this;if(void 0===r||""===r||null===r)return n;for(var i=String(r).split("."),o=0;o<i.length;o++){if(null==n)return e;if(void 0===(n=n[i[o]]))return e}return n},missing:function(){for(var t=[],e=Array.isArray(arguments[0])?arguments[0]:arguments,n=0;n<e.length;n++){var i=e[n],o=r.apply({var:i},this);null!==o&&""!==o||t.push(i)}return t},missing_some:function(t,e){var n=r.apply({missing:e},this);return e.length-n.length>=t?[]:n}};return r.is_logic=function(r){return"object"==typeof r&&null!==r&&!Array.isArray(r)&&1===Object.keys(r).length},r.truthy=function(r){return!(Array.isArray(r)&&0===r.length||!r)},r.get_operator=function(r){return Object.keys(r)[0]},r.get_values=function(t){return t[r.get_operator(t)]},r.apply=function(e,n){if(Array.isArray(e))return e.map((function(t){return r.apply(t,n)}));if(!r.is_logic(e))return e;var i,o,u,a,f,l=r.get_operator(e),c=e[l];if(Array.isArray(c)||(c=[c]),"if"===l||"?:"==l){for(i=0;i<c.length-1;i+=2)if(r.truthy(r.apply(c[i],n)))return r.apply(c[i+1],n);return c.length===i+1?r.apply(c[i],n):null}if("and"===l){for(i=0;i<c.length;i+=1)if(o=r.apply(c[i],n),!r.truthy(o))return o;return o}if("or"===l){for(i=0;i<c.length;i+=1)if(o=r.apply(c[i],n),r.truthy(o))return o;return o}if("filter"===l)return a=r.apply(c[0],n),u=c[1],Array.isArray(a)?a.filter((function(t){return r.truthy(r.apply(u,t))})):[];if("map"===l)return a=r.apply(c[0],n),u=c[1],Array.isArray(a)?a.map((function(t){return r.apply(u,t)})):[];if("reduce"===l)return a=r.apply(c[0],n),u=c[1],f=void 0!==c[2]?c[2]:null,Array.isArray(a)?a.reduce((function(t,e){return r.apply(u,{current:e,accumulator:t})}),f):f;if("all"===l){if(a=r.apply(c[0],n),u=c[1],!Array.isArray(a)||!a.length)return!1;for(i=0;i<a.length;i+=1)if(!r.truthy(r.apply(u,a[i])))return!1;return!0}if("none"===l){if(a=r.apply(c[0],n),u=c[1],!Array.isArray(a)||!a.length)return!0;for(i=0;i<a.length;i+=1)if(r.truthy(r.apply(u,a[i])))return!1;return!0}if("some"===l){if(a=r.apply(c[0],n),u=c[1],!Array.isArray(a)||!a.length)return!1;for(i=0;i<a.length;i+=1)if(r.truthy(r.apply(u,a[i])))return!0;return!1}if(c=c.map((function(t){return r.apply(t,n)})),t.hasOwnProperty(l)&&"function"==typeof t[l])return t[l].apply(n,c);if(l.indexOf(".")>0){var p=String(l).split("."),s=t;for(i=0;i<p.length;i++){if(!s.hasOwnProperty(p[i]))throw new Error("Unrecognized operation "+l+" (failed at "+p.slice(0,i+1).join(".")+")");s=s[p[i]]}return s.apply(n,c)}throw new Error("Unrecognized operation "+l)},r.uses_data=function(t){var e=[];if(r.is_logic(t)){var n=r.get_operator(t),i=t[n];Array.isArray(i)||(i=[i]),"var"===n?e.push(i[0]):i.forEach((function(t){e.push.apply(e,r.uses_data(t))}))}return function(r){for(var t=[],e=0,n=r.length;e<n;e++)-1===t.indexOf(r[e])&&t.push(r[e]);return t}(e)},r.add_operation=function(r,e){t[r]=e},r.rm_operation=function(r){delete t[r]},r.rule_like=function(t,e){if(e===t)return!0;if("@"===e)return!0;if("number"===e)return"number"==typeof t;if("string"===e)return"string"==typeof t;if("array"===e)return Array.isArray(t)&&!r.is_logic(t);if(r.is_logic(e)){if(r.is_logic(t)){var n=r.get_operator(e),i=r.get_operator(t);if("@"===n||n===i)return r.rule_like(r.get_values(t,!1),r.get_values(e,!1))}return!1}if(Array.isArray(e)){if(Array.isArray(t)){if(e.length!==t.length)return!1;for(var o=0;o<e.length;o+=1)if(!r.rule_like(t[o],e[o]))return!1;return!0}return!1}return!1},r},void 0===(i=n.call(t,e,t,r))||(r.exports=i)}},t={},function e(n){var i=t[n];if(void 0!==i)return i.exports;var o=t[n]={exports:{}};return r[n].call(o.exports,o,o.exports,e),o.exports}(193);var r,t})); \ No newline at end of file diff --git a/libs/js/index.ts b/libs/js/index.ts deleted file mode 100644 index a10978834..000000000 --- a/libs/js/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as jsonLogic from 'json-logic-js'; -import {deepMerge} from './utils/deepMerge'; -import {compareSemanticIsGreater} from './utils/operations' -import {IObject, Dimension, DimensionConfig} from './types' - - -export class Config{ - - dimension : Array<Dimension>; - overrides : IObject; - defaultConfig : IObject; - - - static { - jsonLogic.add_operation(">>",compareSemanticIsGreater); - } - - constructor(dimension : DimensionConfig, overrides : IObject, defaultConfig : IObject) { - this.dimension = dimension.dimensions; - this.overrides = overrides; - this.defaultConfig = defaultConfig; - } - - - public evaluateConfig(data : IObject) : IObject { - - const requiredOverrides : Array<IObject> = []; - for(let i = 0; i < this.dimension.length; i++) { - if(jsonLogic.apply(this.dimension[i].condition, data)) { - requiredOverrides.push( - ...this.dimension[i].overrideWithKeys.map(x => this.overrides[x]) - ); - } - } - - const targetConfig : IObject = {...this.defaultConfig}; - return deepMerge(targetConfig , ...requiredOverrides); - } -} diff --git a/libs/js/package-lock.json b/libs/js/package-lock.json index 89223b275..0db0213ac 100644 --- a/libs/js/package-lock.json +++ b/libs/js/package-lock.json @@ -1,31 +1,10513 @@ { - "name": "test", - "version": "0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@types/json-logic-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", - "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", - "dev": true + "name": "Cac-Client", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "Cac-Client", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "json-logic-js": "^2.0.2" + }, + "devDependencies": { + "@babel/core": "^7.22.10", + "@babel/preset-env": "^7.22.10", + "@types/json-logic-js": "^2.0.1", + "@types/node": "^18.11.9", + "babel-core": "^6.26.3", + "babel-loader": "^9.1.3", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", + "eslint": "^8.47.0", + "ts-loader": "^9.4.4", + "typescript": "^4.9.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.19", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/json-logic-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", + "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-core/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/babel-core/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-core/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "dependencies": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "node_modules/babel-generator/node_modules/jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha512-TYX2QQATKA6Wssp6j7jqlw4QLmABDN1olRdEHndYvBXdaXM5dcx6j5rN0+nd+aVL+Th40fAEYvvw/Xxd/LETuQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==", + "dev": true, + "dependencies": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==", + "dev": true, + "dependencies": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha512-SFbWewr0/0U4AiRzsHqwsbOQeLXVa9T1ELdqEa2efcQB5KopTnunAqoj07TuHlN2lfTQNPGO/rJR4FMln5fVcA==", + "dev": true, + "dependencies": { + "babel-helper-bindify-decorators": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==", + "dev": true, + "dependencies": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==", + "dev": true, + "dependencies": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha512-EbciFN5Jb9iqU9bqaLmmFLx2G8pAUsvpWJ6OzOWBNrSY9qTohXj+7YfZx6Ug1Qqh7tCb1EA7Jvn9bMC1HBiucg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha512-EEuBcXz/wZ81Jaac0LnMHtD4Mfz9XWn2oH2Xj+CHwz2SZWUqqdtR2BgWPSdTGMmxN/5KLSh4PImt9+9ZedDarA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha512-AWj19x2aDm8qFQ5O2JcD6pwJDW1YdcnO+1b81t7gxrGjz5VHiUqeYWAR4h7zueWMalRelrQDXprv2FrY1dbpbw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha512-HD/5qJB9oSXzl0caxM+aRD7ENICXqcc3Up/8toDQk7zNIDE7TzsqtxC5f4t9Rwhu2Ya8l9l4j6b3vOsy+a6qxg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==", + "dev": true + }, + "node_modules/babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha512-m8yMoh9LIiNyeLdQs5I9G+3YXo4nqVsKQkk7YplrG4qAFbNi9hkZlow8HDHxhH9QOVFPHmy8+03NzRCdyChIKw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "node_modules/babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==", + "dev": true + }, + "node_modules/babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==", + "dev": true, + "dependencies": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-generators": "^6.5.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==", + "dev": true, + "dependencies": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha512-RvYukT1Nh7njz8P8326ztpQUGCKwmjgu6aRIx1lkvylWITYcskg29vy1Kp8WXIq7FvhXsz0Crf2kS94bjB690A==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-class-constructor-call": "^6.18.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha512-skQ2CImwDkCHu0mkWvCOlBCpBIHW4/49IZWVwV4A/EnWjL9bB6UBvLyMNe3Td5XDStSZNhe69j4bfEW8dvUbew==", + "dev": true, + "dependencies": { + "babel-helper-explode-class": "^6.24.1", + "babel-plugin-syntax-decorators": "^6.13.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha512-yQwYqYg+Tnj1InA8W1rsItsZVhkv1Euc4KVua9ledtPz5PDWYz7LVyy6rDBpVYUWFZj5k6GUm3YZpCbIm8Tqew==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-do-expressions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==", + "dev": true, + "dependencies": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==", + "dev": true, + "dependencies": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "dependencies": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==", + "dev": true, + "dependencies": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==", + "dev": true, + "dependencies": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==", + "dev": true, + "dependencies": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==", + "dev": true, + "dependencies": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==", + "dev": true, + "dependencies": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==", + "dev": true, + "dependencies": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==", + "dev": true, + "dependencies": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-export-extensions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha512-9Ec4KYf1GurT39mlUjDSlN7HWSlB3u3mWRMogQbb+Y88lO0ZM3rJ0ADhPnQwWK9TbO6e/4E+Et1rrfGY9mFimA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-function-bind": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==", + "dev": true, + "dependencies": { + "regenerator-transform": "^0.10.0" + } + }, + "node_modules/babel-plugin-transform-regenerator/node_modules/regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "node_modules/babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + } + }, + "node_modules/babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha512-XfwUqG1Ry6R43m4Wfob+vHbIVBIqTg/TJY4Snku1iIzeH7mUnwHA8Vagmv+ZQbPwhS8HgsdQvy28Py3k5zpoFQ==", + "deprecated": "🙌 Thanks for using Babel: we recommend using babel-preset-env now: please read https://babeljs.io/env to update!", + "dev": true, + "dependencies": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==", + "dev": true, + "dependencies": { + "babel-plugin-transform-do-expressions": "^6.22.0", + "babel-plugin-transform-function-bind": "^6.22.0", + "babel-preset-stage-1": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha512-rn+UOcd7BHDniq1SVxv2/AVVSVI1NK+hfS0I/iR6m6KbOi/aeBRcqBilqO73pd9VUpRXF2HFtlDuC9F2BEQqmg==", + "dev": true, + "dependencies": { + "babel-plugin-transform-class-constructor-call": "^6.24.1", + "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-preset-stage-2": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha512-9F+nquz+37PrlTSBdpeQBKnQfAMNBnryXw+m4qBh35FNbJPfzZz+sjN2G5Uf1CRedU9PH7fJkTbYijxmkLX8Og==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-preset-stage-3": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha512-eCbEOF8uN0KypFXJmZXn2sTk7bPV9uM5xov7G/7BM08TbQEObsVs0cEWfy6NQySlfk7JBi/t+XJP1JkruYfthA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-exponentiation-operator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.22.0" + } + }, + "node_modules/babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==", + "dev": true, + "dependencies": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/babel-traverse/node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-traverse/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babel-types/node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.11", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz", + "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001538", + "electron-to-chromium": "^1.4.526", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001538", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", + "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.527", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.527.tgz", + "integrity": "sha512-EafxEiEDzk2aLrdbtVczylHflHdHkNrpGNHIgDyA63sUQLQVS2ayj2hPw3RsVB42qkwURH+T2OxV7kGPUuYszA==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", - "dev": true - }, - "json-logic-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", - "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" - }, - "typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", - "dev": true + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true + }, + "@babel/core": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + } + }, + "@babel/helpers": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "requires": {} + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-env": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.19", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + } + }, + "@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", + "to-fast-properties": "^2.0.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/json-logic-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", + "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true + } + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha512-TYX2QQATKA6Wssp6j7jqlw4QLmABDN1olRdEHndYvBXdaXM5dcx6j5rN0+nd+aVL+Th40fAEYvvw/Xxd/LETuQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha512-SFbWewr0/0U4AiRzsHqwsbOQeLXVa9T1ELdqEa2efcQB5KopTnunAqoj07TuHlN2lfTQNPGO/rJR4FMln5fVcA==", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "requires": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha512-EbciFN5Jb9iqU9bqaLmmFLx2G8pAUsvpWJ6OzOWBNrSY9qTohXj+7YfZx6Ug1Qqh7tCb1EA7Jvn9bMC1HBiucg==", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha512-EEuBcXz/wZ81Jaac0LnMHtD4Mfz9XWn2oH2Xj+CHwz2SZWUqqdtR2BgWPSdTGMmxN/5KLSh4PImt9+9ZedDarA==", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha512-AWj19x2aDm8qFQ5O2JcD6pwJDW1YdcnO+1b81t7gxrGjz5VHiUqeYWAR4h7zueWMalRelrQDXprv2FrY1dbpbw==", + "dev": true + }, + "babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha512-HD/5qJB9oSXzl0caxM+aRD7ENICXqcc3Up/8toDQk7zNIDE7TzsqtxC5f4t9Rwhu2Ya8l9l4j6b3vOsy+a6qxg==", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==", + "dev": true + }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg==", + "dev": true + }, + "babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha512-m8yMoh9LIiNyeLdQs5I9G+3YXo4nqVsKQkk7YplrG4qAFbNi9hkZlow8HDHxhH9QOVFPHmy8+03NzRCdyChIKw==", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-generators": "^6.5.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha512-RvYukT1Nh7njz8P8326ztpQUGCKwmjgu6aRIx1lkvylWITYcskg29vy1Kp8WXIq7FvhXsz0Crf2kS94bjB690A==", + "dev": true, + "requires": { + "babel-plugin-syntax-class-constructor-call": "^6.18.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha512-skQ2CImwDkCHu0mkWvCOlBCpBIHW4/49IZWVwV4A/EnWjL9bB6UBvLyMNe3Td5XDStSZNhe69j4bfEW8dvUbew==", + "dev": true, + "requires": { + "babel-helper-explode-class": "^6.24.1", + "babel-plugin-syntax-decorators": "^6.13.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha512-yQwYqYg+Tnj1InA8W1rsItsZVhkv1Euc4KVua9ledtPz5PDWYz7LVyy6rDBpVYUWFZj5k6GUm3YZpCbIm8Tqew==", + "dev": true, + "requires": { + "babel-plugin-syntax-do-expressions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg==", + "dev": true, + "requires": { + "babel-plugin-syntax-export-extensions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha512-9Ec4KYf1GurT39mlUjDSlN7HWSlB3u3mWRMogQbb+Y88lO0ZM3rJ0ADhPnQwWK9TbO6e/4E+Et1rrfGY9mFimA==", + "dev": true, + "requires": { + "babel-plugin-syntax-function-bind": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + }, + "dependencies": { + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + } + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha512-XfwUqG1Ry6R43m4Wfob+vHbIVBIqTg/TJY4Snku1iIzeH7mUnwHA8Vagmv+ZQbPwhS8HgsdQvy28Py3k5zpoFQ==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==", + "dev": true, + "requires": { + "babel-plugin-transform-do-expressions": "^6.22.0", + "babel-plugin-transform-function-bind": "^6.22.0", + "babel-preset-stage-1": "^6.24.1" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha512-rn+UOcd7BHDniq1SVxv2/AVVSVI1NK+hfS0I/iR6m6KbOi/aeBRcqBilqO73pd9VUpRXF2HFtlDuC9F2BEQqmg==", + "dev": true, + "requires": { + "babel-plugin-transform-class-constructor-call": "^6.24.1", + "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-preset-stage-2": "^6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha512-9F+nquz+37PrlTSBdpeQBKnQfAMNBnryXw+m4qBh35FNbJPfzZz+sjN2G5Uf1CRedU9PH7fJkTbYijxmkLX8Og==", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-preset-stage-3": "^6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha512-eCbEOF8uN0KypFXJmZXn2sTk7bPV9uM5xov7G/7BM08TbQEObsVs0cEWfy6NQySlfk7JBi/t+XJP1JkruYfthA==", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-exponentiation-operator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.22.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.11", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz", + "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001538", + "electron-to-chromium": "^1.4.526", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001538", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", + "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.10" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "electron-to-chromium": { + "version": "1.4.527", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.527.tgz", + "integrity": "sha512-EafxEiEDzk2aLrdbtVczylHflHdHkNrpGNHIgDyA63sUQLQVS2ayj2hPw3RsVB42qkwURH+T2OxV7kGPUuYszA==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true + }, + "es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "requires": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "requires": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "requires": { + "find-up": "^6.3.0" + }, + "dependencies": { + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==", + "dev": true + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true + }, + "ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } } - } } diff --git a/libs/js/package.json b/libs/js/package.json index 4af52fdc6..c7c4e467a 100644 --- a/libs/js/package.json +++ b/libs/js/package.json @@ -1,21 +1,37 @@ { - "name": "test", - "version": "0.0.1", - "description": "", - "main": "index.js", - "scripts": { - "test": "node ./test/index.js", - "compile": "npx tsc" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "json-logic-js": "^2.0.2" - }, - "devDependencies": { - "@types/json-logic-js": "^2.0.1", - "@types/node": "^18.11.9", - "typescript": "^4.9.3" + "name": "Cac-Client", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "node ./test/index.js", + "compile": "npx tsc", + "dev": "tsc --project tsconfig.json", + "buildLib": "webpack --mode=production", + "build": "run-s secure dev buildLib", + "start": "nodemon ./dist/app.js", + "lint": "eslint . --ext .ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "json-logic-js": "^2.0.2" + }, + "devDependencies": { + "@babel/core": "^7.22.10", + "@babel/preset-env": "^7.22.10", + "@types/json-logic-js": "^2.0.1", + "@types/node": "^18.11.9", + "babel-core": "^6.26.3", + "babel-loader": "^9.1.3", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", + "eslint": "^8.47.0", + "ts-loader": "^9.4.4", + "typescript": "^4.9.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + } } -} diff --git a/libs/js/src/index.ts b/libs/js/src/index.ts new file mode 100644 index 000000000..7b1c1ab18 --- /dev/null +++ b/libs/js/src/index.ts @@ -0,0 +1,41 @@ +import * as jsonLogic from 'json-logic-js'; +import { deepMerge } from './utils/deepMerge'; +import { compareSemanticIsGreater } from './utils/operations' +import { IObject, Dimension } from './types' + +type DataFromCacApi = { + contexts: Array<Dimension>; + overrides: IObject; + default_configs: IObject; +} + +export class CacReader { + contexts: Array<Dimension>; + overrides: IObject; + defaultConfig: IObject; + + static { + jsonLogic.add_operation(">>", compareSemanticIsGreater); + } + + constructor(completeConfig: DataFromCacApi) { + this.contexts = completeConfig.contexts; + this.overrides = completeConfig.overrides; + this.defaultConfig = completeConfig.default_configs; + } + + public evaluateConfig(data: IObject): IObject { + + const requiredOverrides: Array<IObject> = []; + for (let i = 0; i < this.contexts.length; i++) { + if (jsonLogic.apply(this.contexts[i].condition, data)) { + requiredOverrides.push( + ...this.contexts[i].override_with_keys.map(x => this.overrides[x]) + ); + } + } + + const targetConfig: IObject = { ...this.defaultConfig }; + return deepMerge(targetConfig, ...requiredOverrides); + } +} diff --git a/libs/js/src/types.ts b/libs/js/src/types.ts new file mode 100644 index 000000000..e9a023701 --- /dev/null +++ b/libs/js/src/types.ts @@ -0,0 +1,16 @@ +export type IObject = { + [key: string]: any; +} + +export type Dimension = { + condition: IObject, + override_with_keys: Array<string> +} + +export type DimensionConfig = { + dimensions: Array<Dimension> +}; + +export type IIsObject = { + (item: any): boolean; +} \ No newline at end of file diff --git a/libs/js/src/utils/deepMerge.ts b/libs/js/src/utils/deepMerge.ts new file mode 100644 index 000000000..456813bd8 --- /dev/null +++ b/libs/js/src/utils/deepMerge.ts @@ -0,0 +1,44 @@ +import { IIsObject, IObject } from '../types' + + +const isObject: IIsObject = (item: any): boolean => { + return (item === Object(item) && !Array.isArray(item)); +}; + +export const deepMerge = (target: IObject, ...sources: Array<IObject>): IObject => { + // return the target if no sources passed + if (!sources.length) { + return target; + } + + const result: IObject = target; + + if (isObject(result)) { + const len: number = sources.length; + + for (let i = 0; i < len; i += 1) { + const elm: any = sources[i]; + + if (isObject(elm)) { + for (const key in elm) { + if (elm.hasOwnProperty(key)) { + if (isObject(elm[key])) { + if (!result[key] || !isObject(result[key])) { + result[key] = {}; + } + deepMerge(result[key], elm[key]); + } else { + if (Array.isArray(result[key]) && Array.isArray(elm[key])) { + result[key] = [...elm[key]]; + } else { + result[key] = elm[key]; + } + } + } + } + } + } + } + + return result; +}; diff --git a/libs/js/src/utils/operations.ts b/libs/js/src/utils/operations.ts new file mode 100644 index 000000000..9484e5e2b --- /dev/null +++ b/libs/js/src/utils/operations.ts @@ -0,0 +1,9 @@ +const compareSemanticIsGreaterImp = function (version_a: string, version_b: string) { + return version_a.localeCompare(version_b, undefined, {numeric:true, sensitivity:'base'}) > 0; +} + +export const compareSemanticIsGreater = function (version_a: string, version_b: string, version_c?: string) { + return (version_c === undefined ? + compareSemanticIsGreaterImp(version_a, version_b) : + (compareSemanticIsGreaterImp(version_a, version_b) && compareSemanticIsGreaterImp(version_b, version_c))); +} diff --git a/libs/js/tsconfig.json b/libs/js/tsconfig.json index 92f153064..d2bb6eebc 100644 --- a/libs/js/tsconfig.json +++ b/libs/js/tsconfig.json @@ -1,14 +1,16 @@ { - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "lib": ["es6"], - "allowJs": false, - "outDir": "build", - "rootDir": "src", - "strict": true, - "noImplicitAny": true, - "esModuleInterop": true, - "resolveJsonModule": true + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": ["es6"], + "allowJs": false, + "outDir": "./dist", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["./src/**/*"], + "exclude": ["node_modules"] } -} diff --git a/libs/js/types.ts b/libs/js/types.ts deleted file mode 100644 index fef70d725..000000000 --- a/libs/js/types.ts +++ /dev/null @@ -1,19 +0,0 @@ - -export type IObject = { - [key: string]: any; -} - -export type Dimension = { - condition : IObject, - overrideWithKeys : Array<string> -} - -export type DimensionConfig = { - dimensions : Array<Dimension> -}; - - -export type IIsObject = { - (item: any): boolean; -} - diff --git a/libs/js/utils/deepMerge.ts b/libs/js/utils/deepMerge.ts deleted file mode 100644 index e5ed19eda..000000000 --- a/libs/js/utils/deepMerge.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {IIsObject, IObject} from '../types' - - -const isObject: IIsObject = (item: any): boolean => { - return (item === Object(item) && !Array.isArray(item)); -}; - -export const deepMerge = (target: IObject, ...sources: Array<IObject>) : IObject => { - // return the target if no sources passed - if (!sources.length) { - return target; - } - - const result: IObject = target; - - if (isObject(result)) { - const len: number = sources.length; - - for (let i = 0; i < len; i += 1) { - const elm: any = sources[i]; - - if (isObject(elm)) { - for (const key in elm) { - if (elm.hasOwnProperty(key)) { - if (isObject(elm[key])) { - if (!result[key] || !isObject(result[key])) { - result[key] = {}; - } - deepMerge(result[key], elm[key]); - } else { - if (Array.isArray(result[key]) && Array.isArray(elm[key])) { - result[key] = [...elm[key]]; - } else { - result[key] = elm[key]; - } - } - } - } - } - } - } - - return result; -}; diff --git a/libs/js/utils/operations.ts b/libs/js/utils/operations.ts deleted file mode 100644 index c37c4838f..000000000 --- a/libs/js/utils/operations.ts +++ /dev/null @@ -1,15 +0,0 @@ -const compareSemanticIsGreaterImp = function(version_a : string, version_b : string) { - const x = version_a.split("."); - const y = version_b.split("."); - for(let i = 0; i < x.length; i++) { - if(x[i] > y[i]) - return true; - } - return false; -} - -export const compareSemanticIsGreater = function(version_a : string, version_b: string, version_c ?: string) { - return (version_c === undefined ? - compareSemanticIsGreaterImp(version_a, version_b) : - (compareSemanticIsGreaterImp(version_a, version_b) && compareSemanticIsGreaterImp(version_b, version_c))); -} diff --git a/libs/js/webpack.config.js b/libs/js/webpack.config.js new file mode 100644 index 000000000..16e818cb6 --- /dev/null +++ b/libs/js/webpack.config.js @@ -0,0 +1,36 @@ +const path = require('path'); + +module.exports = { + entry: './dist/index.js', + output: { + path: path.resolve(__dirname), + filename: "index.js", + library : { + name : "Context-Aware-Config", + type: "umd" + }, + environment: { + arrowFunction: false, + bigIntLiteral: false, + const: true, + destructuring: false, + dynamicImport: false, + forOf: true, + module: true + } + }, + module: { + rules: [ + { test: /\.js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env"], + } + } + }, + ] + } +}; + From 6a68f71e4472287d1e41801d6429725b2648941a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Mon, 18 Dec 2023 15:57:06 +0530 Subject: [PATCH 193/352] fix: fix json schema validation --- crates/context-aware-config/src/helpers.rs | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index ef4674ebe..2e4917541 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -45,14 +45,22 @@ pub fn get_default_config_validation_schema() -> JSONSchema { } }, "then": { - "properties": { - "pattern": { - "type": "string" - } - }, - "required": [ - "pattern" - ] + "oneOf": [ + { + "required": ["pattern"], + "properties": { "pattern": { "type": "string" } } + }, + { + "required": ["enum"], + "properties": { + "enum": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 1 + }, + } + } + ] } } // TODO: Add validations for Array types. From 8e7db1f3801316fc2f437b57b7b9a9375abd9cfb Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Thu, 21 Dec 2023 13:00:04 +0530 Subject: [PATCH 194/352] fix: array validation for in condition --- .../src/api/context/handlers.rs | 29 ++++--- crates/context-aware-config/src/helpers.rs | 80 +++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index af81c0202..74018cec2 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,4 +1,4 @@ -use crate::helpers::json_to_sorted_string; +use crate::helpers::{json_to_sorted_string, validate_context_jsonschema}; use crate::{ api::{ context::types::{ @@ -49,6 +49,7 @@ pub fn endpoints() -> Scope { type DBConnection = PooledConnection<ConnectionManager<PgConnection>>; fn validate_dimensions_and_calculate_priority( + object_key: &str, cond: &Value, dimension_schema_map: &HashMap<String, (JSONSchema, i32)>, ) -> Result<i32, String> { @@ -65,7 +66,7 @@ fn validate_dimensions_and_calculate_priority( )) .copied() } else { - validate_dimensions_and_calculate_priority(val, dimension_schema_map) + validate_dimensions_and_calculate_priority(key, val, dimension_schema_map) } }; @@ -98,17 +99,19 @@ fn validate_dimensions_and_calculate_priority( .get(&expected_dimension_name) .ok_or("No matching `dimension` for in dimension table")?; - dimension_value_schema - .validate(&dimension_value) - .map_err(|e| { - let verrors = e.collect::<Vec<ValidationError>>(); - String::from(format!("Bad schema: {:?}", verrors.as_slice())) - })?; - }; - + validate_context_jsonschema( + object_key, + &dimension_value, + &dimension_value_schema, + )?; + } arr.iter().try_fold(0, |acc, item| { - validate_dimensions_and_calculate_priority(item, dimension_schema_map) - .map(|res| res + acc) + validate_dimensions_and_calculate_priority( + object_key, + item, + dimension_schema_map, + ) + .map(|res| res + acc) }) } _ => Ok(0), @@ -170,6 +173,7 @@ fn create_ctx_from_put_req( let dimension_schema_map = get_all_dimension_schema_map(conn)?; let priority = match validate_dimensions_and_calculate_priority( + "context", &ctx_condition, &dimension_schema_map, ) { @@ -287,6 +291,7 @@ fn r#move( let new_ctx_id = hash(&ctx_condition); let dimension_schema_map = get_all_dimension_schema_map(conn)?; let priority = match validate_dimensions_and_calculate_priority( + "context", &ctx_condition, &dimension_schema_map, ) { diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index 2e4917541..6687ca349 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -132,12 +132,60 @@ pub fn get_meta_schema() -> JSONSchema { .expect("Error encountered: Failed to compile 'context_dimension_schema_value'. Ensure it adheres to the correct format and data type.") } +pub fn validate_context_jsonschema( + object_key: &str, + dimension_value: &Value, + dimension_schema: &JSONSchema, +) -> Result<(), String> { + match dimension_value { + Value::Array(val_arr) if object_key == "in" => { + let mut verrors = Vec::new(); + val_arr.into_iter().for_each(|x| { + dimension_schema + .validate(&x) + .map_err(|e| { + verrors.append(&mut e.collect::<Vec<ValidationError>>()); + }) + .ok(); + }); + if verrors.is_empty() { + Ok(()) + } else { + // Check if the array as a whole validates, even with individual errors + match dimension_schema.validate(&dimension_value) { + Ok(()) => { + log::error!( + "Validation errors for individual dimensions, but array as a whole validates: {:?}", + verrors + ); + Ok(()) + } + Err(e) => { + verrors.append(&mut e.collect::<Vec<ValidationError>>()); + log::error!( + "Validation errors for dimensions in array: {:?}", + verrors + ); + Err(format!("Bad schema: {:?}", verrors)) + } + } + } + } + _ => dimension_schema.validate(dimension_value).map_err(|e| { + let verrors = e.collect::<Vec<ValidationError>>(); + log::error!("Validation error: {:?}", verrors); + format!("Bad schema: {:?}", verrors) + }), + } +} + /* This step is required because an empty object is also a valid JSON schema. So added required validations for the input. */ // TODO: Recursive validation. + pub fn validate_jsonschema( validation_schema: &JSONSchema, schema: &Value, @@ -287,4 +335,36 @@ mod tests { json_to_sorted_string(&second_condition) ); } + + #[test] + fn test_validate_context_jsonschema() { + let test_schema = json!({ + "type": "string", + "pattern": ".*" + }); + let test_jsonschema = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&test_schema) + .expect("Error encountered: Failed to compile 'context_dimension_schema_value'. Ensure it adheres to the correct format and data type."); + + let str_dimension_val = json!("string1".to_owned()); + let arr_dimension_val = json!(["string1".to_owned(), "string2".to_owned()]); + let ok_str_context = + validate_context_jsonschema("in", &str_dimension_val, &test_jsonschema); + let ok_arr_context = + validate_context_jsonschema("in", &arr_dimension_val, &test_jsonschema); + let err_arr_context = + match validate_context_jsonschema("==", &arr_dimension_val, &test_jsonschema) + { + Ok(_) => false, + Err(e) => { + print!("{:?}", e); + e.contains("Bad schema") + } + }; + + assert_eq!(ok_str_context, Ok(())); + assert_eq!(err_arr_context, true); + assert_eq!(ok_arr_context, Ok(())); + } } From e4d4ef8e61c38a316b3bef7c719b782f9fac7d2b Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 27 Dec 2023 11:27:14 +0000 Subject: [PATCH 195/352] chore(version): v0.16.3 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 7 +++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95646659f..1b8c65364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.16.3 - 2023-12-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.2 +### Global changes +#### Bug Fixes +- PICAF-24552 - Cac client library changes to consume backend api response - (5998ccd) - Prasanna P + +- - - + ## v0.16.2 - 2023-11-30 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.8.2 diff --git a/Cargo.lock b/Cargo.lock index 9029d24e8..8ef2e3467 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,7 +857,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.13.1" +version = "0.13.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 281aa3aea..2aaeb9d41 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.13.2 - 2023-12-27 +#### Bug Fixes +- [PICAF-25568] array validation for in condition - (a45ac4a) - Pratik Mishra +- PICAF-24961 fix json schema validation - (ed6f814) - ankit.mahato + +- - - + ## context-aware-config-v0.13.1 - 2023-11-22 #### Bug Fixes - PICAF-25066 sort json while context creation - (3bd7a97) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 4f8bcd41e..c0641b848 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.13.1" +version = "0.13.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 7e87921da45a54d36b831e5daeb6ab4c65c2082a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 16 Nov 2023 16:47:01 +0530 Subject: [PATCH 196/352] feat: ui for cac and exp --- Cargo.lock | 4 + Cargo.toml | 11 +- crates/context-aware-config/src/main.rs | 9 +- crates/frontend/Cargo.toml | 11 +- crates/frontend/src/app.rs | 246 +--- crates/frontend/src/components/mod.rs | 3 + .../frontend/src/components/nav_item/mod.rs | 1 + .../src/components/nav_item/nav_item.rs | 34 + .../frontend/src/components/side_nav/mod.rs | 1 + .../src/components/side_nav/side_nav.rs | 94 ++ crates/frontend/src/components/table/mod.rs | 2 + crates/frontend/src/components/table/table.rs | 82 ++ crates/frontend/src/components/table/types.rs | 35 + crates/frontend/src/hoc/layout/layout.rs | 15 + crates/frontend/src/hoc/layout/mod.rs | 1 + crates/frontend/src/hoc/mod.rs | 1 + crates/frontend/src/lib.rs | 4 + .../pages/ExperimentList/ExperimentList.rs | 149 +++ .../frontend/src/pages/ExperimentList/mod.rs | 3 + .../src/pages/ExperimentList/types.rs | 50 + .../src/pages/ExperimentList/utils.rs | 54 + crates/frontend/src/pages/Home/Home.rs | 203 ++++ crates/frontend/src/pages/Home/mod.rs | 1 + .../frontend/src/pages/NotFound/NotFound.rs | 21 + crates/frontend/src/pages/NotFound/mod.rs | 1 + crates/frontend/src/pages/mod.rs | 3 + crates/frontend/src/types.rs | 10 + crates/frontend/style/tailwind.css | 4 - crates/frontend/styles/output.css | 535 +++++++++ crates/frontend/styles/tailwind.css | 13 + crates/frontend/tailwind.config.js | 16 +- makefile | 3 + package-lock.json | 1016 ++++++++++++++++- package.json | 4 +- 34 files changed, 2389 insertions(+), 251 deletions(-) create mode 100644 crates/frontend/src/components/mod.rs create mode 100644 crates/frontend/src/components/nav_item/mod.rs create mode 100644 crates/frontend/src/components/nav_item/nav_item.rs create mode 100644 crates/frontend/src/components/side_nav/mod.rs create mode 100644 crates/frontend/src/components/side_nav/side_nav.rs create mode 100644 crates/frontend/src/components/table/mod.rs create mode 100644 crates/frontend/src/components/table/table.rs create mode 100644 crates/frontend/src/components/table/types.rs create mode 100644 crates/frontend/src/hoc/layout/layout.rs create mode 100644 crates/frontend/src/hoc/layout/mod.rs create mode 100644 crates/frontend/src/hoc/mod.rs create mode 100644 crates/frontend/src/pages/ExperimentList/ExperimentList.rs create mode 100644 crates/frontend/src/pages/ExperimentList/mod.rs create mode 100644 crates/frontend/src/pages/ExperimentList/types.rs create mode 100644 crates/frontend/src/pages/ExperimentList/utils.rs create mode 100644 crates/frontend/src/pages/Home/Home.rs create mode 100644 crates/frontend/src/pages/Home/mod.rs create mode 100644 crates/frontend/src/pages/NotFound/NotFound.rs create mode 100644 crates/frontend/src/pages/NotFound/mod.rs create mode 100644 crates/frontend/src/pages/mod.rs create mode 100644 crates/frontend/src/types.rs delete mode 100644 crates/frontend/style/tailwind.css create mode 100644 crates/frontend/styles/output.css create mode 100644 crates/frontend/styles/tailwind.css diff --git a/Cargo.lock b/Cargo.lock index 8ef2e3467..0686add1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1465,7 +1465,9 @@ dependencies = [ "actix-files", "actix-web", "cfg-if", + "chrono", "console_error_panic_hook", + "derive_more", "futures", "http", "leptos", @@ -1475,6 +1477,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "strum", + "strum_macros", "wasm-bindgen", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 7db796ad5..854441077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,16 @@ output-name = "frontend" bin-package = "context-aware-config" lib-package = "frontend" assets-dir = "crates/frontend/assets" -style-file = "frontend/style/tailwind.css" +style-file = "frontend/style/output.css" site-root = "crates/frontend" +[workspace.metadata.leptos.style] +# Main style file. If scss or sass then it will be compiled to css. +# the parent folder will be watched for changes +file = "style/output.css" +# A https://browsersl.ist query +browserquery = "defaults" + [workspace.dependencies] dotenv = "0.15.0" actix = "0.13.0" @@ -46,4 +53,4 @@ rand = "0.8.5" once_cell = { version = "1.18.0" } anyhow = { version = "1.0", default-features = false } # juspay dependencies -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} \ No newline at end of file diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 5cc16ddbc..d26591535 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -194,10 +194,10 @@ async fn main() -> Result<()> { ), ) .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}/assets"))) // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) - // serve other assets from the `assets` directory - .service(Files::new("/assets", site_root)) // serve the favicon from /favicon.ico .service(favicon) .leptos_routes( @@ -216,6 +216,11 @@ async fn main() -> Result<()> { .await } +#[actix_web::get("/style.css")] +async fn css() -> impl actix_web::Responder { + actix_files::NamedFile::open_async("./style/output.css").await +} + fn authenticated_routes() -> AuthenticatedRouteList { let mut route_vector: Vec<(&str, AuthenticatedRoute)> = Vec::new(); route_vector.append(&mut auth::contexts::authenticated_routes()); diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 183745461..e8e7ec888 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -22,7 +22,10 @@ serde = {version = "^1", features = ["derive"]} serde_json = {version = "1.0"} web-sys = "0.3.64" futures = "0.3" - +derive_more = {workspace = true} +strum_macros = "^0.24" +strum = {version = "^0.24"} +chrono = { version = "0.4.26", features = ["serde"] } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -34,8 +37,4 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", -] - - - - +] \ No newline at end of file diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 8a001d259..a6f30b720 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -1,41 +1,11 @@ use leptos::*; use leptos_meta::*; use leptos_router::*; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -#[derive(Deserialize, Serialize, Clone)] -pub struct Config { - pub contexts: Vec<Context>, - pub overrides: Map<String, Value>, - pub default_configs: Map<String, Value>, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Context { - pub id: String, - pub condition: Value, - pub override_with_keys: [String; 1], -} - -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; - let url = format!("{host}/config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} +use crate::hoc::layout::layout::Layout; +use crate::pages::ExperimentList::ExperimentList::ExperimentList; +use crate::pages::{Home::Home::Home, NotFound::NotFound::NotFound}; +use crate::types::AppRoute; #[component] pub fn App(cx: Scope) -> impl IntoView { @@ -44,205 +14,23 @@ pub fn App(cx: Scope) -> impl IntoView { view! { cx, // injects a stylesheet into the document <head> // id=leptos means cargo-leptos will hot-reload this stylesheet - <Stylesheet id="leptos" href="/pkg/tailwind.css"/> - <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> - <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> + <Stylesheet id="leptos" href="/pkg/style.css"/> + // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> + <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> + <Link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet" /> // sets the document title <Title text="Welcome to Context Aware Config"/> // content for this welcome page <Router> - <main> - <Routes> - <Route ssr=SsrMode::PartiallyBlocked path="" view=HomePage/> - <Route path="/*any" view=NotFound/> - </Routes> - </main> + <body class="m-0 min-h-screen bg-gray-50"> + <Layout> + <Routes> + <Route ssr=SsrMode::PartiallyBlocked path="/admin/experiments" view=ExperimentList /> + <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> + <Route path="/*any" view=NotFound/> + </Routes> + </Layout> + </body> </Router> } } - -#[component] -fn HomePage(cx: Scope) -> impl IntoView { - let query = use_query_map(cx); - - let tenant = - query.with(|params_map| params_map.get("tenant").cloned().unwrap_or_default()); - let config_data = - create_blocking_resource(cx, || {}, move |_| fetch_config(tenant.clone())); - - view! { cx, - <div class="container mt-5" > - <div class="text-center mb-4"> - <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> - </div> - <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> - { - config_data.with(cx, move |result| { - match result { - Ok(config) => { - let rows = |k:&String, v:&Value| { - let key = k.replace("\"", "").trim().to_string(); - let value = format!("{}", v).replace("\"", "").trim().to_string(); - view! { cx, - <tr> - <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> - <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> - </tr> - } - }; - - let contexts_views: Vec<_> = config.contexts.iter().map(|context| { - let condition = extract_and_format(&context.condition); - let rows: Vec<_> = context.override_with_keys.iter() - .filter_map(|key| config.overrides.get(key)) - .flat_map(|ovr| ovr.as_object().unwrap().iter()) - .map(|(k, v)| { - rows(&k,&v) - }).collect(); - - view! { cx, - <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> - <table class="table table-responsive table-bordered table-hover border-secondary"> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody class="bg-light"> - { rows } - </tbody> - </table> - } - }).collect::<Vec<_>>(); - - let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); - let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ - rows(&k,&v) - }).collect(); - - vec![ - view! { cx, - <div class="mb-4 "> - { new_context_views } - <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> - <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody> - {default_config} - </tbody> - </table> - </div> - } - ] - }, - Err(error) => { - vec![ - view! { cx, - <div class="error"> - {"Failed to fetch config data: "} - {error} - </div> - } - ] - } - } - }) - } - </Suspense> - </div> - - - } -} -/// 404 - Not Found -#[component] -fn NotFound(cx: Scope) -> impl IntoView { - // set an HTTP status code 404 - // this is feature gated because it can only be done during - // initial server-side rendering - // if you navigate to the 404 page subsequently, the status - // code will not be set because there is not a new HTTP request - // to the server - #[cfg(feature = "ssr")] - { - // this can be done inline because it's synchronous - // if it were async, we'd use a server function - let resp = expect_context::<leptos_actix::ResponseOptions>(cx); - resp.set_status(actix_web::http::StatusCode::NOT_FOUND); - } - view! { cx, - <h1>"Not Found"</h1> - } -} - -pub fn extract_and_format(condition: &Value) -> String { - if condition.is_object() && condition.get("and").is_some() { - // Handling complex "and" conditions - let empty_vec = vec![]; - let conditions_json = condition - .get("and") - .and_then(|val| val.as_array()) - .unwrap_or(&empty_vec); // Default to an empty vector if not an array - - let mut formatted_conditions = Vec::new(); - for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); - } - - formatted_conditions.join(" and ") - } else { - // Handling single conditions - format_condition(condition) - } -} - -fn format_condition(condition: &Value) -> String { - if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { - let empty_vec = vec![]; - let operands = condition[operator].as_array().unwrap_or(&empty_vec); - - // Handling the "in" operator differently - if operator.as_str() == "in" { - let left_operand = &operands[0]; - let right_operand = &operands[1]; - - let left_str = if left_operand.is_string() { - format!("\"{}\"", left_operand.as_str().unwrap()) - } else { - format!("{}", left_operand) - }; - - if right_operand.is_object() && right_operand["var"].is_string() { - let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); - } - } - - // Handling regular operators - if let Some(first_operand) = operands.get(0) { - if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); - if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } - } - } - } - } - - "Invalid Condition".to_string() -} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs new file mode 100644 index 000000000..c9f991179 --- /dev/null +++ b/crates/frontend/src/components/mod.rs @@ -0,0 +1,3 @@ +pub mod nav_item; +pub mod side_nav; +pub mod table; diff --git a/crates/frontend/src/components/nav_item/mod.rs b/crates/frontend/src/components/nav_item/mod.rs new file mode 100644 index 000000000..96edba6b5 --- /dev/null +++ b/crates/frontend/src/components/nav_item/mod.rs @@ -0,0 +1 @@ +pub mod nav_item; diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs new file mode 100644 index 000000000..72d43bc8c --- /dev/null +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -0,0 +1,34 @@ +use leptos::*; +use leptos_router::A; + +#[component] +pub fn NavItem( + cx: Scope, + is_active: bool, + href: String, + text: String, + icon: String, +) -> impl IntoView { + let (anchor_class, icon_wrapper_class) = if is_active { + ( + "py-2.5 px-4 flex items-center whitespace-nowrap active".to_string(), + "rounded-lg bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string() + ) + } else { + ( + "py-2.5 px-4 flex items-center whitespace-nowrap".to_string(), + "rounded-lg shadow-xl shadow-slate-300 bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string() + ) + }; + + let icon_class = format!("{} text-lg text-primary", icon); + view! { + cx, + <A href={href} class={anchor_class}> + <div class={icon_wrapper_class}> + <i class={icon_class} /> + </div> + <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> + </A> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/side_nav/mod.rs b/crates/frontend/src/components/side_nav/mod.rs new file mode 100644 index 000000000..14f93bf3c --- /dev/null +++ b/crates/frontend/src/components/side_nav/mod.rs @@ -0,0 +1 @@ +pub mod side_nav; diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs new file mode 100644 index 000000000..d6803537f --- /dev/null +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -0,0 +1,94 @@ +use crate::components::nav_item::nav_item::NavItem; +use crate::types::AppRoute; +use leptos::*; +use leptos_router::{use_location, A}; + +pub fn SideNav(cx: Scope) -> impl IntoView { + let location = use_location(cx); + let (app_routes, set_app_routes) = create_signal( + cx, + vec![ + AppRoute { + key: "/admin/experiments".to_string(), + path: "/admin/experiments".to_string(), + icon: "ri-test-tube-fill".to_string(), + label: "Experiments".to_string(), + }, + AppRoute { + key: "/admin/dimensions".to_string(), + path: "/admin/dimensions".to_string(), + icon: "ri-ruler-2-fill".to_string(), + label: "Dimensions".to_string(), + }, + AppRoute { + key: "/admin/default-config".to_string(), + path: "/admin/default-config".to_string(), + icon: "ri-tools-line".to_string(), + label: "Default Config".to_string(), + }, + AppRoute { + key: "/admin/overrides".to_string(), + path: "/admin/overrides".to_string(), + icon: "ri-guide-fill".to_string(), + label: "Overrides".to_string(), + }, + AppRoute { + key: "/admin/resolve".to_string(), + path: "/admin/resolve".to_string(), + icon: "ri-equalizer-fill".to_string(), + label: "Resolve".to_string(), + }, + ], + ); + + create_effect(cx, move |_| { + let current_path = location.pathname.get(); + + set_app_routes.update(|app_routes| { + for route in app_routes { + if current_path.contains(&route.path) { + route.key = format!("{}-{}", route.path, "active"); + } else { + route.key = route.path.to_string(); + } + } + }) + }); + + view! { + cx, + <div class="max-w-xs z-990 fixed my-4 ml-4 block w-full h-full flex-wrap inset-y-0 items-center justify-between overflow-y-auto rounded-2xl border-0 bg-white p-0 shadow-none -translate-x-full transition-transform duration-200 xl:left-0 xl:translate-x-0 xl:bg-transparent"> + <div class="h-19.5"> + <A href="/" class="block px-8 py-6 m-0 text-sm whitespace-nowrap text-slate-700"> + <span class="ml-1 font-semibold transition-all duration-200"> + Superposition Platform + </span> + </A> + </div> + <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> + <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> + <ul class="menu"> + <For + each=move || app_routes.get() + key=|route: &AppRoute| route.key.to_string() + view=move |cx, route: AppRoute| { + let path = route.path.to_string(); + let is_active = location.pathname.get().contains(&path); + view! { + cx, + <li class="mt-1 w-full"> + <NavItem + href={route.path.to_string()} + icon={route.icon.to_string()} + text={route.label.to_string()} + is_active={is_active} + /> + </li> + } + } + /> + </ul> + </div> + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/table/mod.rs b/crates/frontend/src/components/table/mod.rs new file mode 100644 index 000000000..711bbac4a --- /dev/null +++ b/crates/frontend/src/components/table/mod.rs @@ -0,0 +1,2 @@ +pub mod table; +pub mod types; \ No newline at end of file diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs new file mode 100644 index 000000000..b68ff4908 --- /dev/null +++ b/crates/frontend/src/components/table/table.rs @@ -0,0 +1,82 @@ +use leptos::*; +use super::types::Column; +use serde_json::{json, Map, Value}; + +fn generate_table_row_str(row: &Value) -> String { + match row { + Value::Null => "null".to_string(), + Value::String(rstr) => rstr.to_string(), + Value::Number(rnum) => rnum.to_string(), + Value::Bool(rbool) => rbool.to_string(), + Value::Array(rarr) => rarr + .iter() + .map(|ele| generate_table_row_str(ele)) + .collect::<Vec<String>>() + .join(","), + Value::Object(robj) => json!(robj).to_string(), + } +} + +#[component] +pub fn Table( + cx: Scope, + key_column: String, + table_style: String, + columns: Vec<Column>, + rows: Vec<Map<String, Value>>, +) -> impl IntoView { + view! { + cx, + <div class="overflow-x-auto"> + <table class="table table-zebra"> + <thead> + <tr> + <th></th> + { + columns + .iter() + .filter(|column| !column.hidden) + .map(|column| view! { + cx, + <th class="uppercase">{&column.name}</th> + }) + .collect_view(cx) + } + </tr> + </thead> + <tbody> + { + rows + .iter() + .enumerate() + .map(|(index, row)| view! { + cx, + <tr> + <th>{index + 1}</th> + { + columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + let cname = &column.name; + let value: String = generate_table_row_str( + row + .get(cname) + .unwrap_or(&Value::String("".to_string())) + ); + view! { + cx, + <td>{(column.formatter)(cx, &value, row)}</td> + } + }) + .collect_view(cx) + } + </tr> + }) + .collect_view(cx) + } + </tbody> + </table> + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs new file mode 100644 index 000000000..96c6da203 --- /dev/null +++ b/crates/frontend/src/components/table/types.rs @@ -0,0 +1,35 @@ +use serde_json::{Map, Value}; +use leptos::{View, Scope, view, IntoView}; + +pub type CellFormatter = fn(Scope, &str, &Map<String, Value>) -> View; + +#[derive(Clone, PartialEq)] +pub struct Column { + pub name: String, + pub hidden: bool, + pub formatter: CellFormatter +} + +fn default_formatter(cx:Scope, value: &str, row: &Map<String, Value>) -> View { + view! { + cx, + <span>{value.to_string()}</span> + }.into_view(cx) +} + +impl Column { + pub fn default(name: String) -> Column { + Column{ + name: name, + hidden: false, + formatter: default_formatter + } + } + pub fn new(name: String, hidden: Option<bool>, formatter: Option<CellFormatter>) -> Column { + Column{ + name: name, + hidden: hidden.unwrap_or(false), + formatter: formatter.unwrap_or(default_formatter) + } + } +} \ No newline at end of file diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs new file mode 100644 index 000000000..48941d4ad --- /dev/null +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -0,0 +1,15 @@ +use crate::components::side_nav::side_nav::SideNav; +use leptos::*; + +#[component] +pub fn Layout(cx: Scope, children: Children) -> impl IntoView { + view! { + cx, + <div> + <SideNav /> + <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200"> + {children(cx)} + </main> + </div> + } +} diff --git a/crates/frontend/src/hoc/layout/mod.rs b/crates/frontend/src/hoc/layout/mod.rs new file mode 100644 index 000000000..dd6461994 --- /dev/null +++ b/crates/frontend/src/hoc/layout/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/crates/frontend/src/hoc/mod.rs b/crates/frontend/src/hoc/mod.rs new file mode 100644 index 000000000..dd6461994 --- /dev/null +++ b/crates/frontend/src/hoc/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index ec6114fe2..489fb4b14 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -1,4 +1,8 @@ pub mod app; +pub mod components; +pub mod hoc; +pub mod pages; +pub mod types; use cfg_if::cfg_if; cfg_if! { diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs new file mode 100644 index 000000000..47a72f6f3 --- /dev/null +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -0,0 +1,149 @@ +use leptos::*; +use leptos_router::*; + +use crate::components::table::{types::Column, table::Table}; + +use crate::pages::ExperimentList::types::{ + ExperimentResponse, ExperimentsResponse, ListFilters, +}; + +use super::utils::fetch_experiments; +use serde_json::{Map, Value, json}; + +// pub struct ListFilters { +// pub status: Option<StatusTypes>, +// pub from_date: Option<DateTime<Utc>>, +// pub to_date: Option<DateTime<Utc>>, +// pub page: Option<i64>, +// pub count: Option<i64>, +// } + +#[component] +pub fn ExperimentList(cx: Scope) -> impl IntoView { + // acquire tenant + let tenant = "test".to_string(); + let (filters, set_filters) = create_signal( + cx, + ListFilters { + status: None, + from_date: None, + to_date: None, + page: None, + count: None, + }, + ); + + let table_columns = create_memo( + cx, + move |_| { + vec![ + Column::default("id".to_string()), + Column::default("name".to_string()), + Column::new( + "status".to_string(), + None, + Some(|cx:Scope, value: &str, _| { + let badge_color = match value { + "CREATED" => "badge-info", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-success", + &_ => "info" + }; + let class = format!("badge {}", badge_color); + view!{ + cx, + <div class={class}> + <span class="text-white font-semibold text-xs"> + {value.to_string()} + </span> + </div> + }.into_view(cx) + }) + ), + Column::default("context".to_string()) + ] + } + ); + + let experiments = + create_blocking_resource(cx, move || filters, move |_| fetch_experiments(filters.get())); + // TODO: Add filters + view! { + cx, + <div class="p-8"> + <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> + <div class="py-4"> + <div class="stats shadow"> + <div class="stat"> + <div class="stat-figure text-primary"> + <i class="ri-test-tube-fill text-5xl" /> + </div> + <div class="stat-title">Experiments</div> + { + move || { + experiments.with( + cx, + move |value| { + match value { + Ok(val) => view! { + cx, + <div class="stat-value"> + {val.total_items} + </div> + }.into_view(cx), + Err(_) => view! { + cx, + <div class="stat-value"> + 0 + </div> + }.into_view(cx) + } + } + ) + } + } + </div> + </div> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <h2 class="card-title">Experiments</h2> + <div> + { + move || experiments.with(cx, move |value| { + leptos::log!("{:?}", value); + match value { + Ok(value) => { + // TODO: Why data.clone() works? + let data = value + .data + .iter() + .map(|ele| { + json!(ele) + .as_object() + .unwrap() + .clone() + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + cx, + <Table + table_style="abc".to_string() + rows={data} + key_column="id".to_string() + columns={table_columns.get()} + /> + } + }, + Err(e) => view! {cx, <div>{e}</div> }.into_view(cx), + } + }) + } + </div> + </div> + </div> + </Suspense> + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/mod.rs b/crates/frontend/src/pages/ExperimentList/mod.rs new file mode 100644 index 000000000..298691e82 --- /dev/null +++ b/crates/frontend/src/pages/ExperimentList/mod.rs @@ -0,0 +1,3 @@ +pub mod ExperimentList; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs new file mode 100644 index 000000000..bf5501f19 --- /dev/null +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -0,0 +1,50 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use derive_more::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] +#[strum(serialize_all = "UPPERCASE")] +pub enum ExperimentStatusType { + CREATED, + CONCLUDED, + INPROGRESS, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub last_modified: DateTime<Utc>, + + pub name: String, + pub override_keys: Vec<String>, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, + pub chosen_variant: Option<String>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExperimentsResponse { + pub total_items: i64, + pub total_pages: i64, + pub data: Vec<ExperimentResponse>, +} + +#[derive(Deserialize, Debug, Clone, Deref, DerefMut)] +pub struct StatusTypes(pub Vec<ExperimentStatusType>); + +#[derive(Debug, Clone)] +pub struct ListFilters { + pub status: Option<StatusTypes>, + pub from_date: Option<DateTime<Utc>>, + pub to_date: Option<DateTime<Utc>>, + pub page: Option<i64>, + pub count: Option<i64>, +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs new file mode 100644 index 000000000..9f29e4d8c --- /dev/null +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -0,0 +1,54 @@ +use super::types::{ExperimentsResponse, ListFilters}; +use std::vec::Vec; + +// pub struct ListFilters { +// pub status: Option<StatusTypes>, +// pub from_date: Option<DateTime<Utc>>, +// pub to_date: Option<DateTime<Utc>>, +// pub page: Option<i64>, +// pub count: Option<i64>, +// } + +pub async fn fetch_experiments( + filters: ListFilters, +) -> Result<ExperimentsResponse, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + + let mut query_params = vec![]; + if let Some(status) = filters.status { + let status: Vec<String> = status.iter().map(|val| val.to_string()).collect(); + query_params.push(format!("status={}", status.join(","))); + } + if let Some(from_date) = filters.from_date { + query_params.push(format!("from_date={}", from_date)); + } + if let Some(to_date) = filters.to_date { + query_params.push(format!("to_date={}", to_date)); + } + if let Some(page) = filters.page { + query_params.push(format!("page={}", page)); + } + if let Some(count) = filters.count { + query_params.push(format!("count={}", count)); + } + + let url = format!("{}/experiments?{}", host, query_params.join(",")); + let response: ExperimentsResponse = client + .get(url) + .header("x-tenant", "mjos") + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + Ok(response) +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs new file mode 100644 index 000000000..e035eedc6 --- /dev/null +++ b/crates/frontend/src/pages/Home/Home.rs @@ -0,0 +1,203 @@ +use leptos::*; +use leptos_router::use_query_map; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} + +pub async fn fetch_config(tenant: String) -> Result<Config, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + let url = format!("{host}/config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} + +#[component] +pub fn Home(cx: Scope) -> impl IntoView { + let query = use_query_map(cx); + + let tenant = + query.with(|params_map| params_map.get("tenant").cloned().unwrap_or_default()); + let config_data = + create_blocking_resource(cx, || {}, move |_| fetch_config(tenant.clone())); + + view! { cx, + <div class="container mt-5" > + <div class="text-center mb-4"> + <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> + </div> + <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> + { + config_data.with(cx, move |result| { + match result { + Ok(config) => { + let rows = |k:&String, v:&Value| { + let key = k.replace("\"", "").trim().to_string(); + let value = format!("{}", v).replace("\"", "").trim().to_string(); + view! { cx, + <tr> + <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> + <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> + </tr> + } + }; + + let contexts_views: Vec<_> = config.contexts.iter().map(|context| { + let condition = extract_and_format(&context.condition); + let rows: Vec<_> = context.override_with_keys.iter() + .filter_map(|key| config.overrides.get(key)) + .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .map(|(k, v)| { + rows(&k,&v) + }).collect(); + + view! { cx, + <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> + <table class="table table-responsive table-bordered table-hover border-secondary"> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody class="bg-light"> + { rows } + </tbody> + </table> + } + }).collect::<Vec<_>>(); + + let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); + let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ + rows(&k,&v) + }).collect(); + + vec![ + view! { cx, + <div class="mb-4 "> + { new_context_views } + <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> + <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody> + {default_config} + </tbody> + </table> + </div> + } + ] + }, + Err(error) => { + vec![ + view! { cx, + <div class="error"> + {"Failed to fetch config data: "} + {error} + </div> + } + ] + } + } + }) + } + </Suspense> + </div> + + + } +} diff --git a/crates/frontend/src/pages/Home/mod.rs b/crates/frontend/src/pages/Home/mod.rs new file mode 100644 index 000000000..1629028ea --- /dev/null +++ b/crates/frontend/src/pages/Home/mod.rs @@ -0,0 +1 @@ +pub mod Home; diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs new file mode 100644 index 000000000..e5dcc3c5e --- /dev/null +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -0,0 +1,21 @@ +use leptos::*; + +#[component] +pub fn NotFound(cx: Scope) -> impl IntoView { + // set an HTTP status code 404 + // this is feature gated because it can only be done during + // initial server-side rendering + // if you navigate to the 404 page subsequently, the status + // code will not be set because there is not a new HTTP request + // to the server + #[cfg(feature = "ssr")] + { + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + let resp = expect_context::<leptos_actix::ResponseOptions>(cx); + resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + } + view! { cx, + <h1>"Not Found"</h1> + } +} diff --git a/crates/frontend/src/pages/NotFound/mod.rs b/crates/frontend/src/pages/NotFound/mod.rs new file mode 100644 index 000000000..e96bf6956 --- /dev/null +++ b/crates/frontend/src/pages/NotFound/mod.rs @@ -0,0 +1 @@ +pub mod NotFound; diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs new file mode 100644 index 000000000..f426339ff --- /dev/null +++ b/crates/frontend/src/pages/mod.rs @@ -0,0 +1,3 @@ +pub mod ExperimentList; +pub mod Home; +pub mod NotFound; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs new file mode 100644 index 000000000..02769d434 --- /dev/null +++ b/crates/frontend/src/types.rs @@ -0,0 +1,10 @@ +use derive_more::{Deref, DerefMut}; +use std::vec::Vec; + +#[derive(Clone, Debug)] +pub struct AppRoute { + pub key: String, + pub path: String, + pub icon: String, + pub label: String, +} diff --git a/crates/frontend/style/tailwind.css b/crates/frontend/style/tailwind.css deleted file mode 100644 index 510ff1d53..000000000 --- a/crates/frontend/style/tailwind.css +++ /dev/null @@ -1,4 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; -@tailwind variants; diff --git a/crates/frontend/styles/output.css b/crates/frontend/styles/output.css new file mode 100644 index 000000000..9d1acd383 --- /dev/null +++ b/crates/frontend/styles/output.css @@ -0,0 +1,535 @@ +/* +! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} \ No newline at end of file diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css new file mode 100644 index 000000000..cc622f122 --- /dev/null +++ b/crates/frontend/styles/tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.h-sidenav { + height: calc(100vh - 370px); +} + +.menu li > :not(ul):not(.menu-title):not(details).active { + @apply bg-primary; + @apply text-white; + @apply font-bold; +} \ No newline at end of file diff --git a/crates/frontend/tailwind.config.js b/crates/frontend/tailwind.config.js index 52c58ebfc..5e6d76994 100644 --- a/crates/frontend/tailwind.config.js +++ b/crates/frontend/tailwind.config.js @@ -1,10 +1,16 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: { - files: ["*.html", "./src/**/*.rs"], - }, + content: [ + "*.html", + "./src/app.rs", + "./src/pages/**/*.rs", + "./src/components/**/*.rs", + "./src/hoc/**/*.rs" + ], theme: { extend: {}, }, - plugins: [], - } \ No newline at end of file + plugins: [ + require("daisyui") + ], +} \ No newline at end of file diff --git a/makefile b/makefile index e07d4c9f9..688f31942 100644 --- a/makefile +++ b/makefile @@ -133,4 +133,7 @@ registry-login: --username AWS \ --password-stdin $(REGISTRY_HOST) +tailwind: + cd crates/frontend && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css --watch + default: dev-build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc172b014..25bbf9e29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,21 @@ "name": "context-aware-configuration", "version": "0.0.1", "devDependencies": { - "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" + "daisyui": "^4.3.1", + "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", + "tailwindcss": "^3.3.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@colors/colors": { @@ -27,6 +41,89 @@ "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@postman/form-data": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", @@ -105,6 +202,31 @@ "node": ">=4" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -159,6 +281,12 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -188,12 +316,43 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==", "dev": true }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/brotli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", @@ -203,6 +362,15 @@ "base64-js": "^1.1.2" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -238,6 +406,45 @@ "node": ">=4.0.0" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/cli-progress": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", @@ -349,18 +556,83 @@ "node": ">=8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csv-parse": { "version": "4.16.3", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", "dev": true }, + "node_modules/culori": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.2.0.tgz", + "integrity": "sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.3.1.tgz", + "integrity": "sha512-dCi91VD+57lkoBd10CjdW4wPOeOPYvvzQbxti6xmyQbDMbCeCXwNq2KdoU798I4OsCcD5B+n7yVG7HAgYW+cvw==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -401,6 +673,12 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, "node_modules/directory-tree": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", @@ -417,6 +695,12 @@ "node": ">=10.0" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -469,12 +753,55 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -493,6 +820,18 @@ "node": ">= 10.4.0" } }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-replace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", @@ -520,6 +859,35 @@ "node": "*" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -529,6 +897,38 @@ "assert-plus": "^1.0.0" } }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -582,6 +982,18 @@ "node": ">=4" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-reasons": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", @@ -648,12 +1060,55 @@ "node": ">=0.10.0" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -663,6 +1118,27 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -675,6 +1151,15 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -735,6 +1220,21 @@ "verror": "1.10.0" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/liquid-json": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", @@ -768,7 +1268,29 @@ "node": ">=10" } }, - "node_modules/mime-db": { + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", @@ -804,6 +1326,18 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -828,6 +1362,35 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -886,6 +1449,15 @@ "integrity": "sha512-0yggixNfrA1KcBwvh/Hy2xAS1Wfs9dcg6TdFf2zN7gilcAigMdrtZ4ybrBSXBgLvGDw9V1p2MRnGBMq7XjTWLg==", "dev": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -895,6 +1467,33 @@ "node": "*" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -904,12 +1503,194 @@ "node": ">=6" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/postman-collection": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", @@ -1138,6 +1919,47 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reduce-flatten": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", @@ -1153,6 +1975,56 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1233,6 +2105,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -1314,6 +2195,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1326,6 +2229,18 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/table-layout": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", @@ -1359,12 +2274,88 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/teleport-javascript": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/teleport-javascript/-/teleport-javascript-1.0.0.tgz", "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -1427,6 +2418,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -1499,6 +2496,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -1513,6 +2516,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } } } } diff --git a/package.json b/package.json index 5453043a2..a8787fd3e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "load_cac_tests": "npx newman dir-import postman/cac -o postman/cac.postman_collection.json" }, "devDependencies": { - "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir" + "daisyui": "^4.3.1", + "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", + "tailwindcss": "^3.3.5" } } From 1f4bd77e20d219a441483fa10c22cbdcd817a151 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Mon, 20 Nov 2023 17:39:33 +0530 Subject: [PATCH 197/352] feat: experiment UI --- .env.example | 4 +- Cargo.lock | 323 ++++++------------ Cargo.toml | 2 + crates/context-aware-config/Cargo.toml | 22 +- crates/context-aware-config/src/main.rs | 4 +- crates/frontend/Cargo.toml | 24 +- crates/frontend/src/app.rs | 12 +- .../src/components/nav_item/nav_item.rs | 2 - .../src/components/side_nav/side_nav.rs | 11 +- crates/frontend/src/components/table/table.rs | 13 +- crates/frontend/src/components/table/types.rs | 9 +- crates/frontend/src/hoc/layout/layout.rs | 5 +- crates/frontend/src/lib.rs | 4 +- crates/frontend/src/pages/Experiment/mod.rs | 204 +++++++++++ .../pages/ExperimentList/ExperimentList.rs | 115 +++---- crates/frontend/src/pages/Home/Home.rs | 33 +- .../frontend/src/pages/NotFound/NotFound.rs | 6 +- crates/frontend/src/pages/mod.rs | 1 + crates/service-utils/Cargo.toml | 4 +- makefile | 2 +- 20 files changed, 447 insertions(+), 353 deletions(-) create mode 100644 crates/frontend/src/pages/Experiment/mod.rs diff --git a/.env.example b/.env.example index 33af043c9..67b0ed5a3 100644 --- a/.env.example +++ b/.env.example @@ -16,13 +16,13 @@ ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" HOSTNAME="<application_name>-<deployment_id>-<replicaset>-<pod>" -MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in +MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=false TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/,/default-config" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" diff --git a/Cargo.lock b/Cargo.lock index 0686add1c..ba6d6c1b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -431,9 +437,9 @@ dependencies = [ [[package]] name = "attribute-derive" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c124f12ade4e670107b132722d0ad1a5c9790bcbc1b265336369ea05626b4498" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" dependencies = [ "attribute-derive-macro", "proc-macro2", @@ -443,13 +449,13 @@ dependencies = [ [[package]] name = "attribute-derive-macro" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b217a07446e0fb086f83401a98297e2d81492122f5874db5391bd270a185f88" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" dependencies = [ "collection_literals", "interpolator", - "proc-macro-error", + "manyhow", "proc-macro-utils", "proc-macro2", "quote", @@ -632,28 +638,24 @@ dependencies = [ [[package]] name = "cached" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700" +checksum = "90eb5776f28a149524d1d8623035760b4454ec881e8cf3838fa8d7e1b11254b3" dependencies = [ - "async-trait", "cached_proc_macro", "cached_proc_macro_types", - "futures", "hashbrown 0.13.2", "instant", "once_cell", "thiserror", - "tokio", ] [[package]] name = "cached_proc_macro" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082" +checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" dependencies = [ - "cached_proc_macro_types", "darling", "proc-macro2", "quote", @@ -807,16 +809,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" dependencies = [ "async-trait", - "json5", "lazy_static", "nom", "pathdiff", - "ron", - "rust-ini", "serde", - "serde_json", "toml", - "yaml-rust", ] [[package]] @@ -881,7 +878,7 @@ dependencies = [ "frontend", "futures", "futures-util", - "itertools", + "itertools 0.10.5", "json-patch", "jsonschema", "leptos", @@ -1219,12 +1216,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - [[package]] name = "dotenv" version = "0.15.0" @@ -1237,18 +1228,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "either" version = "1.9.0" @@ -1264,19 +1243,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.32", -] - [[package]] name = "env_logger" version = "0.8.4" @@ -1464,6 +1430,7 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-web", + "anyhow", "cfg-if", "chrono", "console_error_panic_hook", @@ -1664,24 +1631,22 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.3", -] [[package]] name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] [[package]] name = "heck" @@ -1963,6 +1928,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1999,17 +1973,6 @@ dependencies = [ "treediff", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonlogic" version = "0.5.1" @@ -2063,9 +2026,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "leptos" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65154cd0fc2f505a1676b870d5c055dec9dafe4d6081358ef1d7e357d6f222c5" +checksum = "f98f0fe11faa66358ff8c2ee48881c54f8f216ecddabfc5b69cdc2e90c8e337b" dependencies = [ "cfg-if", "leptos_config", @@ -2076,13 +2039,16 @@ dependencies = [ "server_fn", "tracing", "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", ] [[package]] name = "leptos_actix" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a940095989acffbd08f0264e4c7c529a388adbada7dadf26372148c66f221995" +checksum = "7f50f8c459143ef36c6dce5786e33e48d46dfb6829af87c985d65fb3b0b402aa" dependencies = [ "actix-http", "actix-web", @@ -2094,14 +2060,15 @@ dependencies = [ "parking_lot", "regex", "serde_json", + "tokio", "tracing", ] [[package]] name = "leptos_config" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0108f6c8409c99fcf25f4c55a56b4bf9afeeb58f643879bb115d4258b9e22979" +checksum = "e0f0e1a9a583d943b19c740c82a3ec69224c979af90f40738d93ec59ee1475bb" dependencies = [ "config", "regex", @@ -2112,19 +2079,18 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a92b7a30d6e1363233211babdd59fdd983f28dc3aa6aebbd7bfbdd15630c73" +checksum = "111391d1ccbc3355344f90f0893f4137db13a7f98d53fede0a3613c522ebaf19" dependencies = [ "async-recursion", "cfg-if", "drain_filter_polyfill", - "educe", "futures", "getrandom", "html-escape", "indexmap 2.0.2", - "itertools", + "itertools 0.10.5", "js-sys", "leptos_reactive", "once_cell", @@ -2143,9 +2109,9 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef84aede40b027d1a4addd9bd54c89de722272429f7b21da40b04f9ebe5e3b2" +checksum = "a6902fabee84955a85a6cdebf8ddfbfb134091087b172e32ebb26e571d4640ca" dependencies = [ "anyhow", "camino", @@ -2161,9 +2127,9 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bdd4411a987054b1bce1f89c888ea1bfde50f9c956ce7edc0bb5b54deaf1621" +checksum = "eb816f3c809227b090b538994368a756d494c829b07c1bd312d07263552b8c87" dependencies = [ "futures", "leptos", @@ -2175,15 +2141,15 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc27567e059d8ab630a33bf782a81bb2e10178011b8c97c080aafcf09c4e5e0" +checksum = "e68201041cc5af68f7eb35015336827a36c543d87dcf2403117d7244db1f14a0" dependencies = [ "attribute-derive", "cfg-if", "convert_case 0.6.0", "html-escape", - "itertools", + "itertools 0.11.0", "leptos_hot_reload", "prettyplease", "proc-macro-error", @@ -2198,9 +2164,9 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "329c4bbe4191a0bef6514bab827f2ff1a1e69bc2b431e78ac9799e2bdc26ae33" +checksum = "b64d2b4bd0ab25a4897179ee603f2fa8178da6c9f97ef3efd4fa46580fd7efc1" dependencies = [ "cfg-if", "indexmap 2.0.2", @@ -2212,15 +2178,17 @@ dependencies = [ [[package]] name = "leptos_reactive" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b4fc821e6a8646635b721dd58b5604b5c447eb3b21c464b3837cd2063a6b209" +checksum = "282e84ae3e3eb30ab1eb1c881bfeea8a3cb6d6c683dc99f26f2f69ee240b148d" dependencies = [ "base64 0.21.2", "cfg-if", "futures", "indexmap 2.0.2", "js-sys", + "paste", + "pin-project", "rustc-hash", "self_cell", "serde", @@ -2237,19 +2205,21 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f02384aaeff09ba17093305a0dfe8713fb171f7227a8543992a9ce44c75cd" +checksum = "d5ca4422fdfba8af03d347d346f9364a4393ad36e227a018567192395285cf65" dependencies = [ "cached", "cfg-if", "common_macros", "gloo-net", + "itertools 0.11.0", "js-sys", "lazy_static", "leptos", + "leptos_integration_utils", + "leptos_meta", "linear-map", - "log", "lru", "once_cell", "percent-encoding", @@ -2267,9 +2237,9 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc28e6ae7ca7bd36fc865fb844ecb27ddf72a0eb9514b7ee45d0cad6cf930c7d" +checksum = "e67f3810352bab860bcfa85f1760de4bd6e82cd72b14a97779d9168d37661bbf" dependencies = [ "inventory", "lazy_static", @@ -2306,12 +2276,6 @@ dependencies = [ "cc", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2358,11 +2322,34 @@ dependencies = [ [[package]] name = "lru" -version = "0.10.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.1", +] + +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", ] [[package]] @@ -2638,16 +2625,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - [[package]] name = "overload" version = "0.1.1" @@ -2701,51 +2678,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" -[[package]] -name = "pest" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "pest_meta" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" -dependencies = [ - "once_cell", - "pest", - "sha2 0.10.6", -] - [[package]] name = "pin-project" version = "1.1.3" @@ -2818,7 +2750,6 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", "version_check", ] @@ -3057,17 +2988,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ron" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "serde", -] - [[package]] name = "rs-snowflake" version = "0.6.0" @@ -3167,20 +3087,10 @@ dependencies = [ "rusoto_credential", "rustc_version", "serde", - "sha2 0.9.9", + "sha2", "tokio", ] -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3428,9 +3338,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcddd58a35e4fd00f15dac8f2fc08deed175d8178b2c3e615f59a7e7be6fed7" +checksum = "0186f969a1f9572af27159b8273252abf9a6a38934130fe6f3ae0e439d48cf14" dependencies = [ "ciborium", "const_format", @@ -3453,9 +3363,9 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9083155d5a075eda2d08f18663e4789e0d447a1000b225bc4e1746e849c95c8e" +checksum = "1dbc70e4f185ff2b5c11f02a91baf830f33e456e0571d0680d1d76999ed242ed" dependencies = [ "const_format", "proc-macro-error", @@ -3468,9 +3378,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.4.10" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dba6c99de6539ec3193130f764427ead9d784a76ca3126f38e56a6a0b7a2f3d" +checksum = "d8aaf8cf1f5dde82d3f37548732a4852f65d5279b4ae40add5a2a3c9e559f662" dependencies = [ "server_fn_macro", "syn 2.0.32", @@ -3528,17 +3438,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.6", -] - [[package]] name = "sharded-slab" version = "0.1.4" @@ -4044,13 +3943,22 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typed-builder" -version = "0.14.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cba322cb9b7bc6ca048de49e83918223f35e7a86311267013afff257004870" +checksum = "34085c17941e36627a879208083e25d357243812c30e7d7387c3b954f30ade16" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03ca4cb38206e2bef0700092660bb74d696f808514dae47fa1467cbfe26e96e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.32", ] [[package]] @@ -4059,12 +3967,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "unicase" version = "2.6.0" @@ -4549,15 +4451,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.0-rc.1" diff --git a/Cargo.toml b/Cargo.toml index 854441077..2e9064549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,5 +52,7 @@ rusoto_core = "0.48.0" rand = "0.8.5" once_cell = { version = "1.18.0" } anyhow = { version = "1.0", default-features = false } +strum_macros = "^0.24" +strum = {version = "^0.24"} # juspay dependencies dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} \ No newline at end of file diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index c0641b848..4bbab3db5 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] cac_client = { path = "../cac_client" } -frontend = {path ="../frontend" } +frontend = { path = "../frontend" } # env dotenv = { workspace = true } @@ -26,8 +26,8 @@ serde_json = { workspace = true } env_logger = { workspace = true } log = { workspace = true } # to work with enums -strum_macros = "^0.24" -strum = {version = "^0.24"} +strum_macros = { workspace = true } +strum = { workspace = true } derive_more = { workspace = true } # date and time chrono = { workspace = true } @@ -42,11 +42,11 @@ base64 = { workspace = true } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } urlencoding = { workspace = true } jsonschema = { workspace = true } -reqwest = { workspace = true , features = [ "rustls-tls" ] } +reqwest = { workspace = true, features = ["rustls-tls"] } json-patch = { workspace = true } rand = { workspace = true } service-utils = { path = "../service-utils" } -experimentation-platform = { path = "../experimentation-platform"} +experimentation-platform = { path = "../experimentation-platform" } tracing-actix-web = "0.7.6" tracing-log = "0.1.3" tracing = { version = "0.1.37", features = ["valuable"]} @@ -56,13 +56,13 @@ itertools = "0.10.5" futures = "0.3.28" actix-http = "3.3.1" futures-util = "0.3.28" -external = { path = "../external"} +external = { path = "../external" } actix-cors = "0.6.4" -leptos_actix = { version = "0.4.10" } -leptos = { version = "0.4" } -leptos_meta = { version = "0.4" } -leptos_router = { version = "0.4" } +leptos_actix = { version = "0.5.2" } +leptos = { version = "0.5.2" } +leptos_meta = { version = "0.5.2" } +leptos_router = { version = "0.5.2" } actix-files = { version = "0.6" } anyhow = { workspace = true } -dashboard-auth = { workspace = true } +dashboard-auth = { workspace = true } \ No newline at end of file diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index d26591535..9eba6936e 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -113,7 +113,7 @@ async fn main() -> Result<()> { /* Frontend configurations */ let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); // Generate the list of routes in your Leptos App - let routes = generate_route_list(|cx| view! { cx, <App/> }); + let routes = generate_route_list(|| view! { <App/> }); HttpServer::new(move || { let leptos_options = &conf.leptos_options; @@ -203,7 +203,7 @@ async fn main() -> Result<()> { .leptos_routes( leptos_options.to_owned(), routes.to_owned(), - |cx| view! { cx, <App/> }, + || view! { <App/> }, ) .app_data(Data::new(leptos_options.to_owned())) }) diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index e8e7ec888..2a281466e 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -12,20 +12,22 @@ actix-web = { version = "4", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" cfg-if = "1" http = { version = "0.2", optional = true } -leptos = { version = "0.4" } -leptos_meta = { version = "0.4" } -leptos_actix = { version = "0.4", optional = true } -leptos_router = { version = "0.4" } +leptos = { version = "0.5.2" } +leptos_meta = { version = "0.5.2" } +leptos_actix = { version = "0.5.2", optional = true } +leptos_router = { version = "0.5.2" } wasm-bindgen = "=0.2.87" -reqwest = { version = "0.11", features = ["json"] } -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } web-sys = "0.3.64" futures = "0.3" -derive_more = {workspace = true} -strum_macros = "^0.24" -strum = {version = "^0.24"} -chrono = { version = "0.4.26", features = ["serde"] } +derive_more = { workspace = true } +anyhow = { workspace = true } +chrono = {workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } + [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index a6f30b720..19cb0f198 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -4,14 +4,15 @@ use leptos_router::*; use crate::hoc::layout::layout::Layout; use crate::pages::ExperimentList::ExperimentList::ExperimentList; -use crate::pages::{Home::Home::Home, NotFound::NotFound::NotFound}; -use crate::types::AppRoute; +use crate::pages::{ + Experiment::ExperimentPage, Home::Home::Home, NotFound::NotFound::NotFound, +}; #[component] -pub fn App(cx: Scope) -> impl IntoView { +pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(cx); - view! { cx, + provide_meta_context(); + view! { // injects a stylesheet into the document <head> // id=leptos means cargo-leptos will hot-reload this stylesheet <Stylesheet id="leptos" href="/pkg/style.css"/> @@ -27,6 +28,7 @@ pub fn App(cx: Scope) -> impl IntoView { <Routes> <Route ssr=SsrMode::PartiallyBlocked path="/admin/experiments" view=ExperimentList /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> + <Route ssr=SsrMode::PartiallyBlocked path="/ui/experiments/:id" view=ExperimentPage/> <Route path="/*any" view=NotFound/> </Routes> </Layout> diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 72d43bc8c..148d24cf4 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -3,7 +3,6 @@ use leptos_router::A; #[component] pub fn NavItem( - cx: Scope, is_active: bool, href: String, text: String, @@ -23,7 +22,6 @@ pub fn NavItem( let icon_class = format!("{} text-lg text-primary", icon); view! { - cx, <A href={href} class={anchor_class}> <div class={icon_wrapper_class}> <i class={icon_class} /> diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index d6803537f..e0d824818 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -3,10 +3,9 @@ use crate::types::AppRoute; use leptos::*; use leptos_router::{use_location, A}; -pub fn SideNav(cx: Scope) -> impl IntoView { - let location = use_location(cx); +pub fn SideNav() -> impl IntoView { + let location = use_location(); let (app_routes, set_app_routes) = create_signal( - cx, vec![ AppRoute { key: "/admin/experiments".to_string(), @@ -41,7 +40,7 @@ pub fn SideNav(cx: Scope) -> impl IntoView { ], ); - create_effect(cx, move |_| { + create_effect(move |_| { let current_path = location.pathname.get(); set_app_routes.update(|app_routes| { @@ -56,7 +55,6 @@ pub fn SideNav(cx: Scope) -> impl IntoView { }); view! { - cx, <div class="max-w-xs z-990 fixed my-4 ml-4 block w-full h-full flex-wrap inset-y-0 items-center justify-between overflow-y-auto rounded-2xl border-0 bg-white p-0 shadow-none -translate-x-full transition-transform duration-200 xl:left-0 xl:translate-x-0 xl:bg-transparent"> <div class="h-19.5"> <A href="/" class="block px-8 py-6 m-0 text-sm whitespace-nowrap text-slate-700"> @@ -71,11 +69,10 @@ pub fn SideNav(cx: Scope) -> impl IntoView { <For each=move || app_routes.get() key=|route: &AppRoute| route.key.to_string() - view=move |cx, route: AppRoute| { + children=move |route: AppRoute| { let path = route.path.to_string(); let is_active = location.pathname.get().contains(&path); view! { - cx, <li class="mt-1 w-full"> <NavItem href={route.path.to_string()} diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index b68ff4908..99fe06093 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -19,14 +19,12 @@ fn generate_table_row_str(row: &Value) -> String { #[component] pub fn Table( - cx: Scope, key_column: String, table_style: String, columns: Vec<Column>, rows: Vec<Map<String, Value>>, ) -> impl IntoView { view! { - cx, <div class="overflow-x-auto"> <table class="table table-zebra"> <thead> @@ -37,10 +35,9 @@ pub fn Table( .iter() .filter(|column| !column.hidden) .map(|column| view! { - cx, <th class="uppercase">{&column.name}</th> }) - .collect_view(cx) + .collect_view() } </tr> </thead> @@ -50,7 +47,6 @@ pub fn Table( .iter() .enumerate() .map(|(index, row)| view! { - cx, <tr> <th>{index + 1}</th> { @@ -65,15 +61,14 @@ pub fn Table( .unwrap_or(&Value::String("".to_string())) ); view! { - cx, - <td>{(column.formatter)(cx, &value, row)}</td> + <td>{(column.formatter)(&value, row)}</td> } }) - .collect_view(cx) + .collect_view() } </tr> }) - .collect_view(cx) + .collect_view() } </tbody> </table> diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index 96c6da203..9566949ae 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -1,7 +1,7 @@ use serde_json::{Map, Value}; -use leptos::{View, Scope, view, IntoView}; +use leptos::{View, view, IntoView}; -pub type CellFormatter = fn(Scope, &str, &Map<String, Value>) -> View; +pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; #[derive(Clone, PartialEq)] pub struct Column { @@ -10,11 +10,10 @@ pub struct Column { pub formatter: CellFormatter } -fn default_formatter(cx:Scope, value: &str, row: &Map<String, Value>) -> View { +fn default_formatter(value: &str, row: &Map<String, Value>) -> View { view! { - cx, <span>{value.to_string()}</span> - }.into_view(cx) + }.into_view() } impl Column { diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 48941d4ad..85408dd0d 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -2,13 +2,12 @@ use crate::components::side_nav::side_nav::SideNav; use leptos::*; #[component] -pub fn Layout(cx: Scope, children: Children) -> impl IntoView { +pub fn Layout(children: Children) -> impl IntoView { view! { - cx, <div> <SideNav /> <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200"> - {children(cx)} + {children()} </main> </div> } diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 489fb4b14..b049e5bbb 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -16,8 +16,8 @@ cfg_if! { console_error_panic_hook::set_once(); - leptos::mount_to_body(move |cx| { - view! { cx, <App/> } + leptos::mount_to_body(move || { + view! { <App/> } }); } } diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs new file mode 100644 index 000000000..c4054b2cc --- /dev/null +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -0,0 +1,204 @@ +use leptos::*; +use leptos_router::use_params_map; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::debug; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub(crate) enum ExperimentStatusType { + CREATED, + INPROGRESS, + CONCLUDED, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub(crate) enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub override_id: String, + pub context_id: String, + pub overrides: Value, + pub(crate) variant_type: VariantType, +} + +pub type Variants = Vec<Variant>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Experiment { + pub(crate) variants: Variants, + pub(crate) name: String, + pub(crate) id: String, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, + pub(crate) created_by: String, + pub(crate) created_at: DateTime<Utc>, + pub(crate) last_modified: DateTime<Utc>, + pub(crate) chosen_variant: Option<Variant>, +} + +async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .get(format!("http://localhost:8080/experiments/{}", exp_id)) + .header("x-tenant", "mjos") + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + }, + Err(e) => Err(e.to_string()), + } +} + +#[component] +pub fn experiment_page() -> impl IntoView { + let exp_params = use_params_map(); + let experiment_id = move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + let experiment_info = create_resource( + experiment_id, + |exp_id: String| async move { + get_experiment(&exp_id).await + }, + ); + view! { + <Transition + fallback= move || view! {<h1> Loading.... </h1>} > + {move || + match experiment_info.get() { + Some(Ok(experiment)) => experiment_detail_view(&experiment).into_view(), + Some(Err(err)) => view! {<h1>{err.to_string()}</h1>}.into_view(), + None => view! {<h1>No elements </h1>}.into_view(), + } + } + </Transition> + } +} + +fn experiment_detail_view(exp: &Experiment) -> impl IntoView { + view! { + <div class="flex flex-col overflow-x-auto p-2"> + <h1 class="text-4xl pt-4 font-extrabold"> + {&exp.name} + <span class="badge ml-3 mb-1 badge-primary badge-lg">{exp.status.to_string()}</span> + </h1> + + <div class="divider"></div> + + <div class="join m-5"> + <button class="btn join-item"><i class="ri-edit-line"></i>Edit</button> + <button class="btn join-item"><i class="ri-stop-circle-line"></i>Conclude</button> + <button class="btn join-item"><i class="ri-guide-line"></i>Release</button> + <button class="btn join-item"><i class="ri-flight-takeoff-line"></i>Ramp</button> + </div> + + <div class="stats shadow-xl mt-5"> + <div class="stat"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value">{&exp.id}</div> + </div> + <div class="stat"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value">{exp.traffic_percentage}</div> + </div> + <div class="stat"> + <div class="stat-title">Created by</div> + <div class="stat-value">{&exp.created_by}</div> + </div> + <div class="stat"> + <div class="stat-title">Created at</div> + <div class="stat-value">{format!("{}", &exp.created_at.format("%d-%m-%Y %H:%M:%S"))}</div> + </div> + <div class="stat"> + <div class="stat-title">Last Modified</div> + <div class="stat-value">{format!("{}", &exp.last_modified.format("%d-%m-%Y %H:%M:%S"))}</div> + </div> + </div> + + <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row"> + <div class="stat"> + <div class="stat-title">Client ID</div> + <div class="stat-value">cac</div> + </div> + <div class="divider divider-horizontal">&&</div> + <div class="stat"> + <div class="stat-title">OS</div> + <div class="stat-value">android</div> + </div> + </div> + </div> + </div> + + + <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto"> + <table class="table"> + <thead> + <tr class="bg-base-200"> + <th></th> + <th>Key</th> + <th>Variant-1</th> + <th>Variant-2</th> + <th>Variant-3</th> + <th>Variant-4</th> + <th>Variant-5</th> + <th>Control</th> + </tr> + </thead> + <tbody> + <tr> + <th>1</th> + <td>pmTestKey1</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Blue</td> + <td>Blue</td> + </tr> + <tr> + <th>2</th> + <td>pmTestKey2</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Purple</td> + </tr> + <tr> + <th>3</th> + <td>pmTestKey3</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Red</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + } +} diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 47a72f6f3..7963b69dd 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,14 +1,15 @@ use leptos::*; use leptos_router::*; +use leptos::logging::*; -use crate::components::table::{types::Column, table::Table}; +use crate::components::table::{table::Table, types::Column}; use crate::pages::ExperimentList::types::{ ExperimentResponse, ExperimentsResponse, ListFilters, }; use super::utils::fetch_experiments; -use serde_json::{Map, Value, json}; +use serde_json::{json, Map, Value}; // pub struct ListFilters { // pub status: Option<StatusTypes>, @@ -19,59 +20,54 @@ use serde_json::{Map, Value, json}; // } #[component] -pub fn ExperimentList(cx: Scope) -> impl IntoView { +pub fn ExperimentList() -> impl IntoView { // acquire tenant let tenant = "test".to_string(); - let (filters, set_filters) = create_signal( - cx, - ListFilters { - status: None, - from_date: None, - to_date: None, - page: None, - count: None, - }, - ); + let (filters, set_filters) = create_signal(ListFilters { + status: None, + from_date: None, + to_date: None, + page: None, + count: None, + }); - let table_columns = create_memo( - cx, - move |_| { - vec![ - Column::default("id".to_string()), - Column::default("name".to_string()), - Column::new( - "status".to_string(), - None, - Some(|cx:Scope, value: &str, _| { - let badge_color = match value { - "CREATED" => "badge-info", - "INPROGRESS" => "badge-warning", - "CONCLUDED" => "badge-success", - &_ => "info" - }; - let class = format!("badge {}", badge_color); - view!{ - cx, - <div class={class}> - <span class="text-white font-semibold text-xs"> - {value.to_string()} - </span> - </div> - }.into_view(cx) - }) - ), - Column::default("context".to_string()) - ] - } - ); + let table_columns = create_memo(move |_| { + vec![ + Column::default("id".to_string()), + Column::default("name".to_string()), + Column::new( + "status".to_string(), + None, + Some(|value: &str, _| { + let badge_color = match value { + "CREATED" => "badge-info", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-success", + &_ => "info", + }; + let class = format!("badge {}", badge_color); + view! { + <div class={class}> + <span class="text-white font-semibold text-xs"> + {value.to_string()} + </span> + </div> + } + .into_view() + }), + ), + Column::default("context".to_string()), + ] + }); - let experiments = - create_blocking_resource(cx, move || filters, move |_| fetch_experiments(filters.get())); + let experiments = create_blocking_resource( + move || filters, + move |_| fetch_experiments(filters.get()), + ); // TODO: Add filters view! { - cx, <div class="p-8"> - <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> + <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> <div class="py-4"> <div class="stats shadow"> <div class="stat"> @@ -82,21 +78,18 @@ pub fn ExperimentList(cx: Scope) -> impl IntoView { { move || { experiments.with( - cx, move |value| { match value { - Ok(val) => view! { - cx, + Some(Ok(val)) => view! { <div class="stat-value"> {val.total_items} </div> - }.into_view(cx), - Err(_) => view! { - cx, + }.into_view(), + _ => view! { <div class="stat-value"> 0 </div> - }.into_view(cx) + }.into_view() } } ) @@ -110,10 +103,10 @@ pub fn ExperimentList(cx: Scope) -> impl IntoView { <h2 class="card-title">Experiments</h2> <div> { - move || experiments.with(cx, move |value| { - leptos::log!("{:?}", value); + move || experiments.with(move |value| { + log!("{:?}", value); match value { - Ok(value) => { + Some(Ok(value)) => { // TODO: Why data.clone() works? let data = value .data @@ -127,7 +120,6 @@ pub fn ExperimentList(cx: Scope) -> impl IntoView { .collect::<Vec<Map<String, Value>>>() .to_owned(); view! { - cx, <Table table_style="abc".to_string() rows={data} @@ -136,7 +128,8 @@ pub fn ExperimentList(cx: Scope) -> impl IntoView { /> } }, - Err(e) => view! {cx, <div>{e}</div> }.into_view(cx), + Some(Err(e)) => view! {<div>{e}</div> }.into_view(), + None => view! {<div>Loading....</div> }.into_view(), } }) } @@ -146,4 +139,4 @@ pub fn ExperimentList(cx: Scope) -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index e035eedc6..8527a501d 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -103,28 +103,28 @@ fn format_condition(condition: &Value) -> String { } #[component] -pub fn Home(cx: Scope) -> impl IntoView { - let query = use_query_map(cx); +pub fn Home() -> impl IntoView { + let query = use_query_map(); let tenant = query.with(|params_map| params_map.get("tenant").cloned().unwrap_or_default()); let config_data = - create_blocking_resource(cx, || {}, move |_| fetch_config(tenant.clone())); + create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); - view! { cx, + view! { <div class="container mt-5" > <div class="text-center mb-4"> <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> </div> - <Suspense fallback=move || view! {cx, <p>"Loading (Suspense Fallback)..."</p> }> + <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> { - config_data.with(cx, move |result| { + config_data.with(move |result| { match result { - Ok(config) => { + Some(Ok(config)) => { let rows = |k:&String, v:&Value| { let key = k.replace("\"", "").trim().to_string(); let value = format!("{}", v).replace("\"", "").trim().to_string(); - view! { cx, + view! { <tr> <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> @@ -141,7 +141,7 @@ pub fn Home(cx: Scope) -> impl IntoView { rows(&k,&v) }).collect(); - view! { cx, + view! { <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> <table class="table table-responsive table-bordered table-hover border-secondary"> <thead class="table-primary border-secondary"> @@ -163,7 +163,7 @@ pub fn Home(cx: Scope) -> impl IntoView { }).collect(); vec![ - view! { cx, + view! { <div class="mb-4 "> { new_context_views } <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> @@ -182,15 +182,24 @@ pub fn Home(cx: Scope) -> impl IntoView { } ] }, - Err(error) => { + Some(Err(error)) => { vec![ - view! { cx, + view! { <div class="error"> {"Failed to fetch config data: "} {error} </div> } ] + }, + None => { + vec![ + view! { + <div class="error"> + {"No config data fetched"} + </div> + } + ] } } }) diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs index e5dcc3c5e..52b56389c 100644 --- a/crates/frontend/src/pages/NotFound/NotFound.rs +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -1,7 +1,7 @@ use leptos::*; #[component] -pub fn NotFound(cx: Scope) -> impl IntoView { +pub fn NotFound() -> impl IntoView { // set an HTTP status code 404 // this is feature gated because it can only be done during // initial server-side rendering @@ -12,10 +12,10 @@ pub fn NotFound(cx: Scope) -> impl IntoView { { // this can be done inline because it's synchronous // if it were async, we'd use a server function - let resp = expect_context::<leptos_actix::ResponseOptions>(cx); + let resp = expect_context::<leptos_actix::ResponseOptions>(); resp.set_status(actix_web::http::StatusCode::NOT_FOUND); } - view! { cx, + view! { <h1>"Not Found"</h1> } } diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index f426339ff..76d9eba63 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,3 +1,4 @@ pub mod ExperimentList; pub mod Home; pub mod NotFound; +pub mod Experiment; diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 90744415f..e934b74e2 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -17,8 +17,8 @@ rs-snowflake = { workspace = true } #ORM env_logger = { workspace = true } anyhow = { workspace = true } -strum_macros = "^0.24" -strum = {version = "^0.24"} +strum_macros = { workspace = true } +strum = { workspace = true } diesel = { workspace = true } rusoto_kms = { workspace = true } rusoto_signature = { workspace = true } diff --git a/makefile b/makefile index 688f31942..086734c04 100644 --- a/makefile +++ b/makefile @@ -100,7 +100,7 @@ run: sleep 0.5; \ done # make setup - cp .env.example .env + # cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env make cac -e DOCKER_DNS=$(DOCKER_DNS) From d00db1c63579d64519822b8b3dbad9714d03e1c1 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 22 Nov 2023 17:38:31 +0530 Subject: [PATCH 198/352] feat: added experiment-list page --- .../src/api/default_config/handlers.rs | 23 ++- .../src/api/dimension/handlers.rs | 23 ++- .../src/api/dimension/types.rs | 2 +- crates/context-aware-config/src/db/models.rs | 6 +- crates/frontend/src/components/mod.rs | 1 + .../src/components/nav_item/nav_item.rs | 11 +- .../frontend/src/components/pagination/mod.rs | 1 + .../src/components/pagination/pagination.rs | 33 ++++ .../src/components/side_nav/side_nav.rs | 68 ++++--- crates/frontend/src/components/table/mod.rs | 2 +- crates/frontend/src/components/table/table.rs | 4 +- crates/frontend/src/components/table/types.rs | 23 ++- crates/frontend/src/pages/Experiment/mod.rs | 26 +-- .../pages/ExperimentList/ExperimentList.rs | 175 +++++++++++------- .../src/pages/ExperimentList/types.rs | 8 +- .../src/pages/ExperimentList/utils.rs | 10 +- crates/frontend/src/pages/mod.rs | 2 +- crates/frontend/styles/tailwind.css | 5 +- 18 files changed, 265 insertions(+), 158 deletions(-) create mode 100644 crates/frontend/src/components/pagination/mod.rs create mode 100644 crates/frontend/src/components/pagination/pagination.rs diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index a8d2ba27f..0d86a034d 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -5,8 +5,8 @@ use crate::{ }; use actix_web::{ error::{ErrorBadRequest, ErrorInternalServerError}, - put, - web::{self, Data}, + put, get, + web::{self, Data, Json}, HttpResponse, Scope, }; use chrono::Utc; @@ -17,10 +17,15 @@ use diesel::{ }; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; -use service_utils::service::types::{AppState, DbConnection}; +use service_utils::{ + service::types::{AppState, DbConnection}, + types as app +}; pub fn endpoints() -> Scope { - Scope::new("").service(create) + Scope::new("") + .service(create) + .service(get) } #[put("/{key}")] @@ -122,3 +127,13 @@ fn fetch_default_key( .get_result::<(Value, Value)>(conn)?; Ok(res) } + +#[get("")] +async fn get( + db_conn: DbConnection +) -> app::Result<Json<Vec<DefaultConfig>>> { + let DbConnection(mut conn) = db_conn; + + let result: Vec<DefaultConfig> = default_configs.get_results(&mut conn)?; + Ok(Json(result)) +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 6e6639606..42113716c 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -4,18 +4,23 @@ use crate::{ helpers::validate_jsonschema, }; use actix_web::{ - put, - web::{self, Data}, + put, get, + web::{self, Data, Json}, HttpResponse, Scope, }; use chrono::Utc; use dashboard_auth::types::User; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; -use service_utils::service::types::{AppState, DbConnection}; +use service_utils::{ + service::types::{AppState, DbConnection}, + types as app +}; pub fn endpoints() -> Scope { - Scope::new("").service(create) + Scope::new("") + .service(create) + .service(get) } #[put("")] @@ -75,3 +80,13 @@ async fn create( } } } + +#[get("")] +async fn get( + db_conn: DbConnection +) -> app::Result<Json<Vec<Dimension>>> { + let DbConnection(mut conn) = db_conn; + + let result: Vec<Dimension> = dimensions.get_results(&mut conn)?; + Ok(Json(result)) +} \ No newline at end of file diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs index 9b699498c..f0a5c75b6 100644 --- a/crates/context-aware-config/src/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -6,4 +6,4 @@ pub struct CreateReq { pub dimension: String, pub priority: u16, pub schema: Value, -} +} \ No newline at end of file diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 7ec6f2a88..69dddaf94 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -18,7 +18,7 @@ pub struct Context { pub override_: Value, } -#[derive(Queryable, Selectable, Insertable, AsChangeset)] +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(dimension))] pub struct Dimension { @@ -29,7 +29,7 @@ pub struct Dimension { pub schema: Value, } -#[derive(Queryable, Selectable, Insertable, AsChangeset)] +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(key))] pub struct DefaultConfig { @@ -53,4 +53,4 @@ pub struct EventLog { pub original_data: Option<Value>, pub new_data: Option<Value>, pub query: String, -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index c9f991179..8a1c8b858 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,3 +1,4 @@ pub mod nav_item; +pub mod pagination; pub mod side_nav; pub mod table; diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 148d24cf4..142182108 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -8,19 +8,20 @@ pub fn NavItem( text: String, icon: String, ) -> impl IntoView { - let (anchor_class, icon_wrapper_class) = if is_active { + let (anchor_class, icon_wrapper_class, icon_class) = if is_active { ( "py-2.5 px-4 flex items-center whitespace-nowrap active".to_string(), - "rounded-lg bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string() + "rounded-lg bg-primary w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string(), + format!("{} text-lg text-white font-normal", icon) ) } else { ( "py-2.5 px-4 flex items-center whitespace-nowrap".to_string(), - "rounded-lg shadow-xl shadow-slate-300 bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string() + "rounded-lg shadow-xl shadow-slate-300 bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string(), + format!("{} text-lg text-primary font-normal", icon) ) }; - let icon_class = format!("{} text-lg text-primary", icon); view! { <A href={href} class={anchor_class}> <div class={icon_wrapper_class}> @@ -29,4 +30,4 @@ pub fn NavItem( <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/pagination/mod.rs b/crates/frontend/src/components/pagination/mod.rs new file mode 100644 index 000000000..bc8665b62 --- /dev/null +++ b/crates/frontend/src/components/pagination/mod.rs @@ -0,0 +1 @@ +pub mod pagination; diff --git a/crates/frontend/src/components/pagination/pagination.rs b/crates/frontend/src/components/pagination/pagination.rs new file mode 100644 index 000000000..1f119c594 --- /dev/null +++ b/crates/frontend/src/components/pagination/pagination.rs @@ -0,0 +1,33 @@ +use leptos::*; + +#[component] +pub fn Pagination<NF, PF>( + current_page: i64, + total_pages: i64, + next: NF, + previous: PF, +) -> impl IntoView +where + NF: Fn() + 'static, + PF: Fn() + 'static, +{ + view! { + <div class="join"> + <button + class="join-item btn" + on:click=move |_| previous() + > + "«" + </button> + <button class="join-item btn"> + {format!("Page {} / {}", current_page, total_pages)} + </button> + <button + class="join-item btn" + on:click=move |_| next() + > + "»" + </button> + </div> + } +} diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index e0d824818..ec84ab2a0 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -5,40 +5,38 @@ use leptos_router::{use_location, A}; pub fn SideNav() -> impl IntoView { let location = use_location(); - let (app_routes, set_app_routes) = create_signal( - vec![ - AppRoute { - key: "/admin/experiments".to_string(), - path: "/admin/experiments".to_string(), - icon: "ri-test-tube-fill".to_string(), - label: "Experiments".to_string(), - }, - AppRoute { - key: "/admin/dimensions".to_string(), - path: "/admin/dimensions".to_string(), - icon: "ri-ruler-2-fill".to_string(), - label: "Dimensions".to_string(), - }, - AppRoute { - key: "/admin/default-config".to_string(), - path: "/admin/default-config".to_string(), - icon: "ri-tools-line".to_string(), - label: "Default Config".to_string(), - }, - AppRoute { - key: "/admin/overrides".to_string(), - path: "/admin/overrides".to_string(), - icon: "ri-guide-fill".to_string(), - label: "Overrides".to_string(), - }, - AppRoute { - key: "/admin/resolve".to_string(), - path: "/admin/resolve".to_string(), - icon: "ri-equalizer-fill".to_string(), - label: "Resolve".to_string(), - }, - ], - ); + let (app_routes, set_app_routes) = create_signal(vec![ + AppRoute { + key: "/admin/experiments".to_string(), + path: "/admin/experiments".to_string(), + icon: "ri-test-tube-fill".to_string(), + label: "Experiments".to_string(), + }, + AppRoute { + key: "/admin/dimensions".to_string(), + path: "/admin/dimensions".to_string(), + icon: "ri-ruler-2-fill".to_string(), + label: "Dimensions".to_string(), + }, + AppRoute { + key: "/admin/default-config".to_string(), + path: "/admin/default-config".to_string(), + icon: "ri-tools-line".to_string(), + label: "Default Config".to_string(), + }, + AppRoute { + key: "/admin/overrides".to_string(), + path: "/admin/overrides".to_string(), + icon: "ri-guide-fill".to_string(), + label: "Overrides".to_string(), + }, + AppRoute { + key: "/admin/resolve".to_string(), + path: "/admin/resolve".to_string(), + icon: "ri-equalizer-fill".to_string(), + label: "Resolve".to_string(), + }, + ]); create_effect(move |_| { let current_path = location.pathname.get(); @@ -88,4 +86,4 @@ pub fn SideNav() -> impl IntoView { </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/table/mod.rs b/crates/frontend/src/components/table/mod.rs index 711bbac4a..604f33274 100644 --- a/crates/frontend/src/components/table/mod.rs +++ b/crates/frontend/src/components/table/mod.rs @@ -1,2 +1,2 @@ pub mod table; -pub mod types; \ No newline at end of file +pub mod types; diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index 99fe06093..0b83162c4 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -1,5 +1,5 @@ -use leptos::*; use super::types::Column; +use leptos::*; use serde_json::{json, Map, Value}; fn generate_table_row_str(row: &Value) -> String { @@ -74,4 +74,4 @@ pub fn Table( </table> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index 9566949ae..c941ddb59 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -1,5 +1,5 @@ +use leptos::{view, IntoView, View}; use serde_json::{Map, Value}; -use leptos::{View, view, IntoView}; pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; @@ -7,28 +7,33 @@ pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; pub struct Column { pub name: String, pub hidden: bool, - pub formatter: CellFormatter + pub formatter: CellFormatter, } fn default_formatter(value: &str, row: &Map<String, Value>) -> View { view! { <span>{value.to_string()}</span> - }.into_view() + } + .into_view() } impl Column { pub fn default(name: String) -> Column { - Column{ + Column { name: name, hidden: false, - formatter: default_formatter + formatter: default_formatter, } } - pub fn new(name: String, hidden: Option<bool>, formatter: Option<CellFormatter>) -> Column { - Column{ + pub fn new( + name: String, + hidden: Option<bool>, + formatter: Option<CellFormatter>, + ) -> Column { + Column { name: name, hidden: hidden.unwrap_or(false), - formatter: formatter.unwrap_or(default_formatter) + formatter: formatter.unwrap_or(default_formatter), } } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index c4054b2cc..15e68bcc7 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,11 +1,13 @@ +use chrono::{DateTime, Utc}; use leptos::*; use leptos_router::use_params_map; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::debug; -use chrono::{DateTime, Utc}; -#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display)] +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] #[strum(serialize_all = "UPPERCASE")] pub(crate) enum ExperimentStatusType { CREATED, @@ -56,10 +58,10 @@ async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { Ok(experiment) => { debug!("experiment response {:?}", experiment); Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - }, + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } Err(e) => Err(e.to_string()), } } @@ -67,13 +69,11 @@ async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { #[component] pub fn experiment_page() -> impl IntoView { let exp_params = use_params_map(); - let experiment_id = move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); - let experiment_info = create_resource( - experiment_id, - |exp_id: String| async move { - get_experiment(&exp_id).await - }, - ); + let experiment_id = + move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + let experiment_info = create_resource(experiment_id, |exp_id: String| async move { + get_experiment(&exp_id).await + }); view! { <Transition fallback= move || view! {<h1> Loading.... </h1>} > diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 7963b69dd..db7616364 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,8 +1,16 @@ +use leptos::logging::*; use leptos::*; use leptos_router::*; -use leptos::logging::*; -use crate::components::table::{table::Table, types::Column}; +use chrono::{ + prelude::{DateTime, Utc}, + TimeZone, +}; + +use crate::components::{ + pagination::pagination::Pagination, + table::{table::Table, types::Column}, +}; use crate::pages::ExperimentList::types::{ ExperimentResponse, ExperimentsResponse, ListFilters, @@ -11,24 +19,16 @@ use crate::pages::ExperimentList::types::{ use super::utils::fetch_experiments; use serde_json::{json, Map, Value}; -// pub struct ListFilters { -// pub status: Option<StatusTypes>, -// pub from_date: Option<DateTime<Utc>>, -// pub to_date: Option<DateTime<Utc>>, -// pub page: Option<i64>, -// pub count: Option<i64>, -// } - #[component] pub fn ExperimentList() -> impl IntoView { // acquire tenant let tenant = "test".to_string(); let (filters, set_filters) = create_signal(ListFilters { status: None, - from_date: None, - to_date: None, - page: None, - count: None, + from_date: Utc.timestamp_opt(0, 0).single(), + to_date: Utc.timestamp_opt(4130561031, 0).single(), + page: Some(1), + count: Some(1), }); let table_columns = create_memo(move |_| { @@ -61,14 +61,24 @@ pub fn ExperimentList() -> impl IntoView { }); let experiments = create_blocking_resource( - move || filters, - move |_| fetch_experiments(filters.get()), + move || filters.get(), + |value| async move { + match fetch_experiments(value).await { + Ok(data) => data, + Err(e) => ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + }, + } + }, ); + // TODO: Add filters view! { <div class="p-8"> <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> - <div class="py-4"> + <div class="pb-4"> <div class="stats shadow"> <div class="stat"> <div class="stat-figure text-primary"> @@ -77,63 +87,98 @@ pub fn ExperimentList() -> impl IntoView { <div class="stat-title">Experiments</div> { move || { - experiments.with( - move |value| { - match value { - Some(Ok(val)) => view! { - <div class="stat-value"> - {val.total_items} - </div> - }.into_view(), - _ => view! { - <div class="stat-value"> - 0 - </div> - }.into_view() - } - } - ) + let value = experiments.get(); + let total_items = match value { + Some(v) => v.total_items, + _ => 0, + }; + view! { + <div class="stat-value"> + {total_items} + </div> + } } } </div> </div> </div> - <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card rounded-xl w-full bg-base-100 shadow"> <div class="card-body"> <h2 class="card-title">Experiments</h2> - <div> - { - move || experiments.with(move |value| { - log!("{:?}", value); - match value { - Some(Ok(value)) => { - // TODO: Why data.clone() works? - let data = value - .data - .iter() - .map(|ele| { - json!(ele) - .as_object() - .unwrap() - .clone() - }) - .collect::<Vec<Map<String, Value>>>() - .to_owned(); - view! { - <Table - table_style="abc".to_string() - rows={data} - key_column="id".to_string() - columns={table_columns.get()} - /> - } - }, - Some(Err(e)) => view! {<div>{e}</div> }.into_view(), - None => view! {<div>Loading....</div> }.into_view(), + <div> + { + move || { + let value = experiments.get(); + match value { + Some(v) => { + let data = v + .data + .iter() + .map(|ele| { + json!(ele) + .as_object() + .unwrap() + .clone() + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + <Table + table_style="abc".to_string() + rows={data} + key_column="id".to_string() + columns={table_columns.get()} + /> + } + }, + None => { + view! {<div>Loading....</div> }.into_view() } - }) + } } - </div> + + } + </div> + <div class="mt-2 flex justify-end"> + { + move || { + let current_page = filters.get().page.unwrap_or(0); + let total_pages = match experiments.get() { + Some(val) => val.total_pages, + None => 0, + }; + + view! { + <Pagination + current_page={current_page} + total_pages={total_pages} + next={ + move || { + set_filters.update(|f| { + f.page = match f.page { + Some(p) if p < total_pages => Some(p + 1), + Some(p) => Some(p), + None => None, + } + }); + } + } + previous={ + move || { + set_filters.update(|f| { + f.page = match f.page { + Some(p) if p > 1 => Some(p - 1), + Some(p) => Some(p), + None => None, + } + }); + } + } + /> + } + } + } + </div> </div> </div> </Suspense> diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index bf5501f19..12b43e2a0 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -30,21 +30,21 @@ pub struct ExperimentResponse { pub chosen_variant: Option<String>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExperimentsResponse { pub total_items: i64, pub total_pages: i64, pub data: Vec<ExperimentResponse>, } -#[derive(Deserialize, Debug, Clone, Deref, DerefMut)] +#[derive(Deserialize, Debug, Clone, Deref, DerefMut, PartialEq)] pub struct StatusTypes(pub Vec<ExperimentStatusType>); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ListFilters { pub status: Option<StatusTypes>, pub from_date: Option<DateTime<Utc>>, pub to_date: Option<DateTime<Utc>>, pub page: Option<i64>, pub count: Option<i64>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 9f29e4d8c..e21b720b0 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,14 +1,6 @@ use super::types::{ExperimentsResponse, ListFilters}; use std::vec::Vec; -// pub struct ListFilters { -// pub status: Option<StatusTypes>, -// pub from_date: Option<DateTime<Utc>>, -// pub to_date: Option<DateTime<Utc>>, -// pub page: Option<i64>, -// pub count: Option<i64>, -// } - pub async fn fetch_experiments( filters: ListFilters, ) -> Result<ExperimentsResponse, String> { @@ -39,7 +31,7 @@ pub async fn fetch_experiments( query_params.push(format!("count={}", count)); } - let url = format!("{}/experiments?{}", host, query_params.join(",")); + let url = format!("{}/experiments?{}", host, query_params.join("&")); let response: ExperimentsResponse = client .get(url) .header("x-tenant", "mjos") diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 76d9eba63..069062ac9 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,4 +1,4 @@ +pub mod Experiment; pub mod ExperimentList; pub mod Home; pub mod NotFound; -pub mod Experiment; diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index cc622f122..683974950 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -7,7 +7,8 @@ } .menu li > :not(ul):not(.menu-title):not(details).active { - @apply bg-primary; - @apply text-white; + @apply bg-white; + @apply text-black; + @apply shadow-lg; @apply font-bold; } \ No newline at end of file From 24b4cbf0726a1a3e4b08100ba4de59bc6fa5aaa3 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Tue, 5 Dec 2023 15:55:13 +0530 Subject: [PATCH 199/352] feat: dimensions --- crates/frontend/src/app.rs | 2 + .../src/pages/Dimensions/Dimensions.rs | 99 +++++++++++++++++++ .../frontend/src/pages/Dimensions/helper.rs | 27 +++++ crates/frontend/src/pages/Dimensions/mod.rs | 3 + crates/frontend/src/pages/Dimensions/types.rs | 13 +++ crates/frontend/src/pages/mod.rs | 1 + 6 files changed, 145 insertions(+) create mode 100644 crates/frontend/src/pages/Dimensions/Dimensions.rs create mode 100644 crates/frontend/src/pages/Dimensions/helper.rs create mode 100644 crates/frontend/src/pages/Dimensions/mod.rs create mode 100644 crates/frontend/src/pages/Dimensions/types.rs diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 19cb0f198..574a23ff3 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -3,6 +3,7 @@ use leptos_meta::*; use leptos_router::*; use crate::hoc::layout::layout::Layout; +use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::ExperimentList::ExperimentList::ExperimentList; use crate::pages::{ Experiment::ExperimentPage, Home::Home::Home, NotFound::NotFound::NotFound, @@ -29,6 +30,7 @@ pub fn App() -> impl IntoView { <Route ssr=SsrMode::PartiallyBlocked path="/admin/experiments" view=ExperimentList /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> <Route ssr=SsrMode::PartiallyBlocked path="/ui/experiments/:id" view=ExperimentPage/> + <Route ssr=SsrMode::PartiallyBlocked path="/admin/dimensions" view=Dimensions/> <Route path="/*any" view=NotFound/> </Routes> </Layout> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs new file mode 100644 index 000000000..6ad0dda59 --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -0,0 +1,99 @@ +use leptos::logging::*; +use leptos::*; +use serde_json::{json, Map, Value}; + +use crate::components::table::{table::Table, types::Column}; +use crate::pages::Dimensions::helper::fetch_dimensions; + +#[component] +pub fn Dimensions() -> impl IntoView { + let dimensions = create_blocking_resource( + move || {}, + |_value| async move { + match fetch_dimensions().await { + Ok(data) => data, + Err(e) => vec![], + } + }, + ); + + let table_columns = create_memo(move |_| { + vec![ + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), + Column::default("dimension".to_string()), + Column::default("priority".to_string()), + Column::default("schema".to_string()), + ] + }); + + view! { + <div class="p-8"> + <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> + <div class="pb-4"> + <div class="stats shadow"> + <div class="stat"> + <div class="stat-figure text-primary"> + <i class="ri-ruler-2-fill text-5xl" /> + </div> + <div class="stat-title">Dimensions</div> + { + move || { + let value = dimensions.get(); + let total_items = match value { + Some(v) => v.len(), + _ => 0, + }; + view! { + <div class="stat-value"> + {total_items} + </div> + } + } + } + </div> + </div> + </div> + + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <h2 class="card-title">Dimensions</h2> + <div> + { + move || { + let value = dimensions.get(); + match value { + Some(v) => { + let data = v + .iter() + .map(|ele| { + json!(ele) + .as_object() + .unwrap() + .clone() + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + println!("hello1: {:?}", data); + view! { + <Table + table_style="abc".to_string() + rows={data} + key_column="id".to_string() + columns={table_columns.get()} + /> + } + }, + None => { + view! {<div>Loading....</div> }.into_view() + } + } + } + } + </div> + </div> + </div> + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/Dimensions/helper.rs b/crates/frontend/src/pages/Dimensions/helper.rs new file mode 100644 index 000000000..c5f32c7d2 --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/helper.rs @@ -0,0 +1,27 @@ +use std::vec::Vec; + +use super::types::Dimension; + +pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + + let url = format!("{}/dimension", host); + let response: Vec<Dimension> = client + .get(url) + .header("x-tenant", "mjos") + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + Ok(response) +} diff --git a/crates/frontend/src/pages/Dimensions/mod.rs b/crates/frontend/src/pages/Dimensions/mod.rs new file mode 100644 index 000000000..9847bda4a --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/mod.rs @@ -0,0 +1,3 @@ +pub mod Dimensions; +pub mod helper; +pub mod types; diff --git a/crates/frontend/src/pages/Dimensions/types.rs b/crates/frontend/src/pages/Dimensions/types.rs new file mode 100644 index 000000000..b9727c942 --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/types.rs @@ -0,0 +1,13 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use derive_more::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, +} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 069062ac9..d3257d205 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,3 +1,4 @@ +pub mod Dimensions; pub mod Experiment; pub mod ExperimentList; pub mod Home; From 3a6036c9309eec145cc88f080fd8b507c944bb9f Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 29 Nov 2023 07:31:20 +0530 Subject: [PATCH 200/352] feat: override and context form --- .../components/context_form/context_form.rs | 130 ++++++++++++++++++ .../src/components/context_form/mod.rs | 1 + .../experiment_form/experiment_form.rs | 101 ++++++++++++++ .../src/components/experiment_form/mod.rs | 1 + crates/frontend/src/components/mod.rs | 3 + .../src/components/override_form/mod.rs | 1 + .../components/override_form/override_form.rs | 60 ++++++++ .../pages/ExperimentList/ExperimentList.rs | 40 +++++- .../src/pages/ExperimentList/types.rs | 34 +++++ .../src/pages/ExperimentList/utils.rs | 52 ++++++- 10 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 crates/frontend/src/components/context_form/context_form.rs create mode 100644 crates/frontend/src/components/context_form/mod.rs create mode 100644 crates/frontend/src/components/experiment_form/experiment_form.rs create mode 100644 crates/frontend/src/components/experiment_form/mod.rs create mode 100644 crates/frontend/src/components/override_form/mod.rs create mode 100644 crates/frontend/src/components/override_form/override_form.rs diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs new file mode 100644 index 000000000..16272f36e --- /dev/null +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -0,0 +1,130 @@ +use leptos::*; +use std::collections::HashSet; +use crate::pages::ExperimentList::types::Dimension; + +#[component] +pub fn ContextForm( + dimensions: Vec<Dimension>, + context: Vec<(String, String, String)> +) -> impl IntoView { + let (context, set_context) = create_signal(context); + let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); + let total_dimensions = dimensions.len(); + + // please suggest a better way to write this + let last_idx = create_memo(move |_| { + let len = context.get().len(); + if len == 0 { 0 } else { len - 1 } + }); + + view! { + <div class="form-control w-full"> + <label class="label"> + <span class="label-text">Context</span> + </label> + <div class="p-4 bg-white"> + <For + each=move || { context.get().into_iter().enumerate().collect::<Vec<(usize, (String, String, String))>>() } + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) + children=move |(idx, (dimension, operator, value))| { + let dimension_label = dimension.to_string(); + let dimension_name = dimension.to_string(); + view! { + <div class="flex gap-x-6"> + <div class="form-control w-20"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">Operator</span> + </label> + <select class="select select-bordered"> + <option disabled selected>Pick one</option> + <option value="==">"=="</option> + <option value="!=">"!="</option> + </select> + + </div> + <div class="form-control"> + <label class="label capitalize font-mono text-sm"> + <span class="label-text">{dimension_label}</span> + </label> + <div class="flex gap-x-6 items-center"> + <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + <button + class="text-error text-xl font-light font-thin" + on:click={move |_| { + set_context.update(|value| { + value.remove(idx); + }); + set_used_dimensions.update(|value| { + value.remove(&dimension_name); + }); + }} + > + <i class="ri-delete-bin-2-line"></i> + </button> + </div> + </div> + </div> + { + move || { + if last_idx.get() != idx { + view! { + <div class="my-3 ml-5 ml-6 ml-7"> + <span class="font-mono text-xs"> + "&&" + </span> + </div> + }.into_view() + } else { + view! {}.into_view() + } + } + } + } + } + /> + <div class="mt-2"> + <div class="dropdown"> + <label tabindex="0" class="btn btn-circle btn-info text-white btn-sm m-1"> + <i class="ri-add-line font-bold text-2xl"></i> + </label> + <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> + <For + each=move || { + dimensions + .clone() + .into_iter() + .filter(|dim| !used_dimensions.get().contains(&dim.dimension)) + .collect::<Vec<Dimension>>() + } + key=|dimension: &Dimension| dimension.dimension.to_string() + children=move |dimension: Dimension| { + let dimension_name = dimension.dimension.to_string(); + let label = dimension_name.to_string(); + view! { + <li + on:click={ + move |_| { + set_context + .update(|value| { + value + .push((dimension_name.to_string(), "".to_string(), "".to_string())) + }); + set_used_dimensions + .update(|value| { + value.insert(dimension_name.to_string()); + }); + } + } + > + <a>{label.to_string()}</a> + </li> + } + } + /> + </ul> + </div> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/mod.rs b/crates/frontend/src/components/context_form/mod.rs new file mode 100644 index 000000000..bdef1614a --- /dev/null +++ b/crates/frontend/src/components/context_form/mod.rs @@ -0,0 +1 @@ +pub mod context_form; \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs new file mode 100644 index 000000000..fb0a60072 --- /dev/null +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -0,0 +1,101 @@ +use leptos::*; +use serde_json::{Value, Map}; +use std::collections::HashSet; +use crate::pages::ExperimentList::types::{Dimension, DefaultConfig, Variant, VariantType}; +use crate::components::{ + context_form::context_form::ContextForm, + override_form::override_form::OverrideForm, +}; + +#[component] +pub fn ExperimentForm( + name: String, + context: Vec<(String, String, String)>, + variants: Vec<Variant>, + + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig> +) -> impl IntoView { + let (experiment_name, set_experiment_name) = create_signal(name); + let (variants, set_variants) = create_signal(variants); + + view! { + <div> + <h2>Create Experiment</h2> + + <div class="form-control w-full"> + <label class="label"> + <span class="label-text">Name</span> + </label> + <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + </div> + + /*** Context Form **/ + <ContextForm + dimensions={dimensions} + context={context} + /> + /*** Context Form **/ + + /*** OVerride Form **/ + <div> + <label> + <span class="label-text"> + Variants + </span> + </label> + </div> + <For + each=move || variants.get() + key=|variant: &Variant| variant.id.to_string() + children=move |variant: Variant| { + let config = default_config.clone(); + view! { + <div> + <div class="form-control w-full"> + <label class="label"> + <span class="label-text">Id</span> + </label> + <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + </div> + <div class="form-control w-full"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select class="select select-bordered"> + <option disabled selected>Pick one</option> + <option value={VariantType::CONTROL.to_string()}>{VariantType::CONTROL.to_string()}</option> + <option value={VariantType::EXPERIMENTAL.to_string()}>{VariantType::EXPERIMENTAL.to_string()}</option> + </select> + </div> + <div> + <OverrideForm overrides={variant.overrides} default_config={config} /> + </div> + </div> + } + } + /> + // make some more random default id for this can lead to undefined behaviour if we use index based ids + <button + class="btn btn-circle btn-outline" + on:click=move |_| { + leptos::logging::log!("add new variant"); + set_variants.update(|value| { + let total_variants = value.len(); + value.push(Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + }) + }); + } + > + <i class="ri-add-circle-fill"></i> + </button> + + /*** OVerride Form **/ + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/mod.rs b/crates/frontend/src/components/experiment_form/mod.rs new file mode 100644 index 000000000..f91c3a966 --- /dev/null +++ b/crates/frontend/src/components/experiment_form/mod.rs @@ -0,0 +1 @@ +pub mod experiment_form; \ No newline at end of file diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 8a1c8b858..6ae6debaf 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -2,3 +2,6 @@ pub mod nav_item; pub mod pagination; pub mod side_nav; pub mod table; +pub mod context_form; +pub mod override_form; +pub mod experiment_form; \ No newline at end of file diff --git a/crates/frontend/src/components/override_form/mod.rs b/crates/frontend/src/components/override_form/mod.rs new file mode 100644 index 000000000..60a352a7b --- /dev/null +++ b/crates/frontend/src/components/override_form/mod.rs @@ -0,0 +1 @@ +pub mod override_form; \ No newline at end of file diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs new file mode 100644 index 000000000..efd23dbc8 --- /dev/null +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -0,0 +1,60 @@ +use leptos::*; +use serde_json::{Map, Value}; +use std::collections::HashSet; +use crate::pages::ExperimentList::types::{DefaultConfig}; + +#[component] +pub fn OverrideForm( + overrides: Map<String, Value>, + default_config: Vec<DefaultConfig> +) -> impl IntoView { + let (overrides, set_overrides) = create_signal(overrides); + + view! { + <For + each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } + key=|(config_key, _)| config_key.to_string() + children=move |(config_key, config_value)| { + let configs = default_config.clone(); + let config_key_copy = config_value.clone(); + view!{ + <div> + <div class="flex gap-x-6"> + <div class="form-control w-20"> + <select class="select select-bordered"> + <option disabled selected>Select Config</option> + <For + each=move || configs.clone() + key=|item: &DefaultConfig| item.key.to_string() + children=move |item: DefaultConfig| { + view! { + <option value={item.key.to_string()} selected={item.key.to_string() == config_key_copy}> + {item.key} + </option> + } + } + /> + </select> + </div> + <div class="form-control"> + <div class="flex gap-x-6 items-center"> + <input type="text" placeholder="Type here" value={config_value.to_string()} class="input input-bordered w-full max-w-xs" /> + <button + class="text-error text-xl font-light font-thin" + on:click={move |_| { + set_overrides.update(|value| { + value.remove(&config_key); + }); + }} + > + <i class="ri-delete-bin-2-line"></i> + </button> + </div> + </div> + </div> + </div> + } + } + /> + } +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index db7616364..6b73bee4d 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -10,13 +10,14 @@ use chrono::{ use crate::components::{ pagination::pagination::Pagination, table::{table::Table, types::Column}, + experiment_form::experiment_form::ExperimentForm }; use crate::pages::ExperimentList::types::{ ExperimentResponse, ExperimentsResponse, ListFilters, }; -use super::utils::fetch_experiments; +use super::utils::{fetch_default_config, fetch_dimensions, fetch_experiments}; use serde_json::{json, Map, Value}; #[component] @@ -74,6 +75,26 @@ pub fn ExperimentList() -> impl IntoView { }, ); + let dimensions = create_blocking_resource( + || (), + |_| async move { + match fetch_dimensions().await { + Ok(data) => data, + Err(e) => vec![], + } + }, + ); + + let default_config = create_blocking_resource( + || (), + |_| async move { + match fetch_default_config().await { + Ok(data) => data, + Err(e) => vec![], + } + }, + ); + // TODO: Add filters view! { <div class="p-8"> @@ -181,7 +202,22 @@ pub fn ExperimentList() -> impl IntoView { </div> </div> </div> + { + move || { + let dim = dimensions.get().unwrap_or(vec![]); + let def_conf = default_config.get().unwrap_or(vec![]); + view! { + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions={dim} + default_config={def_conf} + /> + } + } + } </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index 12b43e2a0..b0804c91b 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -48,3 +48,37 @@ pub struct ListFilters { pub page: Option<i64>, pub count: Option<i64>, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DefaultConfig { + pub key: String, + pub value: Value, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option<String>, + pub override_id: Option<String>, + pub overrides: Map<String, Value>, +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index e21b720b0..74d4592ab 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,4 +1,4 @@ -use super::types::{ExperimentsResponse, ListFilters}; +use super::types::{ExperimentsResponse, ListFilters, Dimension, DefaultConfig}; use std::vec::Vec; pub async fn fetch_experiments( @@ -44,3 +44,53 @@ pub async fn fetch_experiments( Ok(response) } + + +pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + + let url = format!("{}/dimension", host); + let response: Vec<Dimension> = client + .get(url) + .header("x-tenant", "mjos") + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + Ok(response) +} + + +pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + + let url = format!("{}/default-config", host); + let response: Vec<DefaultConfig> = client + .get(url) + .header("x-tenant", "mjos") + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + Ok(response) +} \ No newline at end of file From bf94e31ffc75ccafe1cb9cfe4f56dca4805de135 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Mon, 20 Nov 2023 17:39:33 +0530 Subject: [PATCH 201/352] feat: working experiments page --- .../src/api/default_config/handlers.rs | 10 +- .../src/api/dimension/handlers.rs | 14 +- .../src/api/dimension/types.rs | 2 +- crates/context-aware-config/src/db/models.rs | 2 +- crates/frontend/src/app.rs | 28 +- .../components/context_form/context_form.rs | 119 ++-- .../src/components/context_form/mod.rs | 2 +- .../experiment_form/experiment_form.rs | 143 +++-- .../src/components/experiment_form/mod.rs | 2 +- crates/frontend/src/components/mod.rs | 6 +- .../src/components/nav_item/nav_item.rs | 6 +- .../src/components/override_form/mod.rs | 2 +- .../components/override_form/override_form.rs | 82 +-- .../src/components/pagination/pagination.rs | 12 +- .../src/components/side_nav/side_nav.rs | 115 ++-- crates/frontend/src/components/table/table.rs | 102 ++-- crates/frontend/src/components/table/types.rs | 12 +- crates/frontend/src/hoc/layout/layout.rs | 2 +- .../src/pages/Dimensions/Dimensions.rs | 79 ++- crates/frontend/src/pages/Experiment/mod.rs | 563 ++++++++++++++---- .../pages/ExperimentList/ExperimentList.rs | 230 +++---- .../src/pages/ExperimentList/types.rs | 2 +- .../src/pages/ExperimentList/utils.rs | 9 +- crates/frontend/src/pages/Home/Home.rs | 202 ++++--- .../frontend/src/pages/NotFound/NotFound.rs | 4 +- crates/frontend/src/types.rs | 4 +- flake.nix | 1 + 27 files changed, 1129 insertions(+), 626 deletions(-) diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 0d86a034d..b907155b8 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -19,13 +19,11 @@ use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; use service_utils::{ service::types::{AppState, DbConnection}, - types as app + types as app, }; pub fn endpoints() -> Scope { - Scope::new("") - .service(create) - .service(get) + Scope::new("").service(create).service(get) } #[put("/{key}")] @@ -129,9 +127,7 @@ fn fetch_default_key( } #[get("")] -async fn get( - db_conn: DbConnection -) -> app::Result<Json<Vec<DefaultConfig>>> { +async fn get(db_conn: DbConnection) -> app::Result<Json<Vec<DefaultConfig>>> { let DbConnection(mut conn) = db_conn; let result: Vec<DefaultConfig> = default_configs.get_results(&mut conn)?; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index 42113716c..bed885220 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -4,7 +4,7 @@ use crate::{ helpers::validate_jsonschema, }; use actix_web::{ - put, get, + get, put, web::{self, Data, Json}, HttpResponse, Scope, }; @@ -14,13 +14,11 @@ use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use service_utils::{ service::types::{AppState, DbConnection}, - types as app + types as app, }; pub fn endpoints() -> Scope { - Scope::new("") - .service(create) - .service(get) + Scope::new("").service(create).service(get) } #[put("")] @@ -82,11 +80,9 @@ async fn create( } #[get("")] -async fn get( - db_conn: DbConnection -) -> app::Result<Json<Vec<Dimension>>> { +async fn get(db_conn: DbConnection) -> app::Result<Json<Vec<Dimension>>> { let DbConnection(mut conn) = db_conn; let result: Vec<Dimension> = dimensions.get_results(&mut conn)?; Ok(Json(result)) -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs index f0a5c75b6..9b699498c 100644 --- a/crates/context-aware-config/src/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -6,4 +6,4 @@ pub struct CreateReq { pub dimension: String, pub priority: u16, pub schema: Value, -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 69dddaf94..1a752ea81 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -53,4 +53,4 @@ pub struct EventLog { pub original_data: Option<Value>, pub new_data: Option<Value>, pub query: String, -} \ No newline at end of file +} diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 574a23ff3..3b7a2c669 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -13,13 +13,17 @@ use crate::pages::{ pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); + let (tenant_rs, tenant_ws) = create_signal(String::from("mjos")); + provide_context(tenant_rs); + provide_context(tenant_ws); view! { - // injects a stylesheet into the document <head> - // id=leptos means cargo-leptos will hot-reload this stylesheet <Stylesheet id="leptos" href="/pkg/style.css"/> // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> - <Link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet" /> + <Link + href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" + rel="stylesheet" + /> // sets the document title <Title text="Welcome to Context Aware Config"/> // content for this welcome page @@ -27,10 +31,22 @@ pub fn App() -> impl IntoView { <body class="m-0 min-h-screen bg-gray-50"> <Layout> <Routes> - <Route ssr=SsrMode::PartiallyBlocked path="/admin/experiments" view=ExperimentList /> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/dimensions" + view=Dimensions + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/experiments" + view=ExperimentList + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/experiments/:id" + view=ExperimentPage + /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> - <Route ssr=SsrMode::PartiallyBlocked path="/ui/experiments/:id" view=ExperimentPage/> - <Route ssr=SsrMode::PartiallyBlocked path="/admin/dimensions" view=Dimensions/> <Route path="/*any" view=NotFound/> </Routes> </Layout> diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 16272f36e..ca9e32e1c 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -1,11 +1,11 @@ +use crate::pages::ExperimentList::types::Dimension; use leptos::*; use std::collections::HashSet; -use crate::pages::ExperimentList::types::Dimension; #[component] pub fn ContextForm( dimensions: Vec<Dimension>, - context: Vec<(String, String, String)> + context: Vec<(String, String, String)>, ) -> impl IntoView { let (context, set_context) = create_signal(context); let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); @@ -14,7 +14,11 @@ pub fn ContextForm( // please suggest a better way to write this let last_idx = create_memo(move |_| { let len = context.get().len(); - if len == 0 { 0 } else { len - 1 } + if len == 0 { + 0 + } else { + len - 1 + } }); view! { @@ -22,9 +26,16 @@ pub fn ContextForm( <label class="label"> <span class="label-text">Context</span> </label> - <div class="p-4 bg-white"> + <div class="p-4"> <For - each=move || { context.get().into_iter().enumerate().collect::<Vec<(usize, (String, String, String))>>() } + each=move || { + context + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, String, String))>>() + } + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) children=move |(idx, (dimension, operator, value))| { let dimension_label = dimension.to_string(); @@ -36,7 +47,9 @@ pub fn ContextForm( <span class="label-text">Operator</span> </label> <select class="select select-bordered"> - <option disabled selected>Pick one</option> + <option disabled selected> + Pick one + </option> <option value="==">"=="</option> <option value="!=">"!="</option> </select> @@ -47,84 +60,98 @@ pub fn ContextForm( <span class="label-text">{dimension_label}</span> </label> <div class="flex gap-x-6 items-center"> - <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + <input + type="text" + placeholder="Type here" + class="input input-bordered w-full max-w-xs" + /> <button class="text-error text-xl font-light font-thin" - on:click={move |_| { - set_context.update(|value| { - value.remove(idx); - }); - set_used_dimensions.update(|value| { - value.remove(&dimension_name); - }); - }} + on:click=move |_| { + set_context + .update(|value| { + value.remove(idx); + }); + set_used_dimensions + .update(|value| { + value.remove(&dimension_name); + }); + } > + <i class="ri-delete-bin-2-line"></i> </button> </div> </div> </div> - { - move || { - if last_idx.get() != idx { - view! { - <div class="my-3 ml-5 ml-6 ml-7"> - <span class="font-mono text-xs"> - "&&" - </span> - </div> - }.into_view() - } else { - view! {}.into_view() + + {move || { + if last_idx.get() != idx { + view! { + <div class="my-3 ml-5 ml-6 ml-7"> + <span class="font-mono text-xs">"&&"</span> + </div> } + .into_view() + } else { + view! {}.into_view() } - } + }} } } /> + <div class="mt-2"> <div class="dropdown"> <label tabindex="0" class="btn btn-circle btn-info text-white btn-sm m-1"> <i class="ri-add-line font-bold text-2xl"></i> </label> - <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> + <ul + tabindex="0" + class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" + > <For each=move || { dimensions .clone() .into_iter() - .filter(|dim| !used_dimensions.get().contains(&dim.dimension)) + .filter(|dim| { + !used_dimensions.get().contains(&dim.dimension) + }) .collect::<Vec<Dimension>>() } + key=|dimension: &Dimension| dimension.dimension.to_string() children=move |dimension: Dimension| { let dimension_name = dimension.dimension.to_string(); let label = dimension_name.to_string(); view! { - <li - on:click={ - move |_| { - set_context - .update(|value| { - value - .push((dimension_name.to_string(), "".to_string(), "".to_string())) - }); - set_used_dimensions - .update(|value| { - value.insert(dimension_name.to_string()); - }); - } - } - > + <li on:click=move |_| { + set_context + .update(|value| { + value + .push(( + dimension_name.to_string(), + "".to_string(), + "".to_string(), + )) + }); + set_used_dimensions + .update(|value| { + value.insert(dimension_name.to_string()); + }); + }> + <a>{label.to_string()}</a> </li> } } /> + </ul> </div> </div> </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/context_form/mod.rs b/crates/frontend/src/components/context_form/mod.rs index bdef1614a..73287c04b 100644 --- a/crates/frontend/src/components/context_form/mod.rs +++ b/crates/frontend/src/components/context_form/mod.rs @@ -1 +1 @@ -pub mod context_form; \ No newline at end of file +pub mod context_form; diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index fb0a60072..65ab87001 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,48 +1,67 @@ -use leptos::*; -use serde_json::{Value, Map}; -use std::collections::HashSet; -use crate::pages::ExperimentList::types::{Dimension, DefaultConfig, Variant, VariantType}; use crate::components::{ - context_form::context_form::ContextForm, - override_form::override_form::OverrideForm, + context_form::context_form::ContextForm, override_form::override_form::OverrideForm, +}; +use crate::pages::ExperimentList::types::{ + DefaultConfig, Dimension, Variant, VariantType, }; +use crate::types::InputVector; +use leptos::html::Form; +use leptos::*; +use serde_json::Map; +use web_sys::SubmitEvent; #[component] pub fn ExperimentForm( name: String, context: Vec<(String, String, String)>, variants: Vec<Variant>, - dimensions: Vec<Dimension>, - default_config: Vec<DefaultConfig> + default_config: Vec<DefaultConfig>, ) -> impl IntoView { + // let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (experiment_name, set_experiment_name) = create_signal(name); let (variants, set_variants) = create_signal(variants); + let input_vector: InputVector = vec![(experiment_name, set_experiment_name)]; + let (iv_rs, iv_ws) = create_signal(input_vector); + let on_submit = move |ev: SubmitEvent| { + ev.prevent_default(); + let document = document(); + let exp_name = document + .get_element_by_id("expName") + .expect("expName input not found"); + logging::log!("{:#?}", exp_name.get_attribute_names()); + let variant_ids = document.get_elements_by_tag_name("variantId"); + for i in 0..variant_ids.length() { + logging::log!("{:#?}", variant_ids.item(i)); + } + let override_vec = document.get_elements_by_tag_name("override"); + for i in 0..override_vec.length() { + logging::log!("{:#?}", override_vec.item(i)); + } + }; view! { - <div> - <h2>Create Experiment</h2> - + <form on:submit=on_submit> <div class="form-control w-full"> <label class="label"> - <span class="label-text">Name</span> + <span class="label-text">Experiment Name</span> </label> - <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + <input + value=move || experiment_name.get() + on:input=move |ev| set_experiment_name.set(event_target_value(&ev)) + type="text" + name="expName" + id="expName" + placeholder="ex. testing hyperpay release" + class="input input-bordered w-full max-w-xs" + /> </div> - /*** Context Form **/ - <ContextForm - dimensions={dimensions} - context={context} - /> - /*** Context Form **/ + <ContextForm dimensions=dimensions context=context/> - /*** OVerride Form **/ <div> <label> - <span class="label-text"> - Variants - </span> + <span class="label-text">Variants</span> </label> </div> <For @@ -50,52 +69,92 @@ pub fn ExperimentForm( key=|variant: &Variant| variant.id.to_string() children=move |variant: Variant| { let config = default_config.clone(); + let (variant_rs, variant_ws) = create_signal(variant); view! { <div> <div class="form-control w-full"> <label class="label"> <span class="label-text">Id</span> </label> - <input type="text" placeholder="Type here" class="input input-bordered w-full max-w-xs" /> + <input + name="variantId[]" + value=move || variant_rs.get().id + type="text" + placeholder="Type a unique name here" + class="input w-full max-w-xs" + /> </div> <div class="form-control w-full"> <label class="label font-medium text-sm"> <span class="label-text">Type</span> </label> - <select class="select select-bordered"> - <option disabled selected>Pick one</option> - <option value={VariantType::CONTROL.to_string()}>{VariantType::CONTROL.to_string()}</option> - <option value={VariantType::EXPERIMENTAL.to_string()}>{VariantType::EXPERIMENTAL.to_string()}</option> + <select + name="expType[]" + value=move || variant_rs.get().variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_rs.get().clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + variant_ws.set(new_variant); + } + + class="select select-bordered" + > + <option disabled selected> + Pick one + </option> + <option value=VariantType::CONTROL + .to_string()>{VariantType::CONTROL.to_string()}</option> + <option value=VariantType::EXPERIMENTAL + .to_string()> + {VariantType::EXPERIMENTAL.to_string()} + </option> </select> </div> <div> - <OverrideForm overrides={variant.overrides} default_config={config} /> + <OverrideForm + iv_rs=iv_rs + iv_ws=iv_ws + overrides=variant_rs.get().overrides + default_config=config + /> </div> </div> } } /> + // make some more random default id for this can lead to undefined behaviour if we use index based ids <button class="btn btn-circle btn-outline" on:click=move |_| { leptos::logging::log!("add new variant"); - set_variants.update(|value| { - let total_variants = value.len(); - value.push(Variant { - id: format!("variant-{}", total_variants), - variant_type: VariantType::EXPERIMENTAL, - context_id: None, - override_id: None, - overrides: Map::new(), - }) - }); + set_variants + .update(|value| { + let total_variants = value.len(); + value + .push(Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + }) + }); } > + <i class="ri-add-circle-fill"></i> </button> + <div class="modal-action"> + <form method="dialog"> + <button class="btn">Save</button> + </form> + </div> - /*** OVerride Form **/ - </div> + </form> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/mod.rs b/crates/frontend/src/components/experiment_form/mod.rs index f91c3a966..ba0d1fc80 100644 --- a/crates/frontend/src/components/experiment_form/mod.rs +++ b/crates/frontend/src/components/experiment_form/mod.rs @@ -1 +1 @@ -pub mod experiment_form; \ No newline at end of file +pub mod experiment_form; diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 6ae6debaf..674effa64 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,7 +1,7 @@ +pub mod context_form; +pub mod experiment_form; pub mod nav_item; +pub mod override_form; pub mod pagination; pub mod side_nav; pub mod table; -pub mod context_form; -pub mod override_form; -pub mod experiment_form; \ No newline at end of file diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 142182108..4ca872413 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -23,9 +23,9 @@ pub fn NavItem( }; view! { - <A href={href} class={anchor_class}> - <div class={icon_wrapper_class}> - <i class={icon_class} /> + <A href=href class=anchor_class> + <div class=icon_wrapper_class> + <i class=icon_class></i> </div> <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> diff --git a/crates/frontend/src/components/override_form/mod.rs b/crates/frontend/src/components/override_form/mod.rs index 60a352a7b..3c1de112d 100644 --- a/crates/frontend/src/components/override_form/mod.rs +++ b/crates/frontend/src/components/override_form/mod.rs @@ -1 +1 @@ -pub mod override_form; \ No newline at end of file +pub mod override_form; diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index efd23dbc8..bfb714eda 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,15 +1,15 @@ +use crate::{pages::ExperimentList::types::DefaultConfig, types::InputVector}; use leptos::*; use serde_json::{Map, Value}; -use std::collections::HashSet; -use crate::pages::ExperimentList::types::{DefaultConfig}; #[component] pub fn OverrideForm( overrides: Map<String, Value>, - default_config: Vec<DefaultConfig> + default_config: Vec<DefaultConfig>, + iv_rs: ReadSignal<InputVector>, + iv_ws: WriteSignal<InputVector>, ) -> impl IntoView { let (overrides, set_overrides) = create_signal(overrides); - view! { <For each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } @@ -17,44 +17,58 @@ pub fn OverrideForm( children=move |(config_key, config_value)| { let configs = default_config.clone(); let config_key_copy = config_value.clone(); - view!{ + view! { <div> - <div class="flex gap-x-6"> - <div class="form-control w-20"> - <select class="select select-bordered"> - <option disabled selected>Select Config</option> - <For - each=move || configs.clone() - key=|item: &DefaultConfig| item.key.to_string() - children=move |item: DefaultConfig| { - view! { - <option value={item.key.to_string()} selected={item.key.to_string() == config_key_copy}> - {item.key} - </option> - } + <div class="flex gap-x-6"> + <div class="form-control w-20"> + <select class="select select-bordered"> + <option disabled selected> + Select Config + </option> + <For + each=move || configs.clone() + key=|item: &DefaultConfig| item.key.to_string() + children=move |item: DefaultConfig| { + view! { + <option + value=item.key.to_string() + selected=item.key.to_string() == config_key_copy + > + {item.key} + </option> } - /> - </select> - </div> - <div class="form-control"> - <div class="flex gap-x-6 items-center"> - <input type="text" placeholder="Type here" value={config_value.to_string()} class="input input-bordered w-full max-w-xs" /> - <button - class="text-error text-xl font-light font-thin" - on:click={move |_| { - set_overrides.update(|value| { + } + /> + + </select> + </div> + <div class="form-control"> + <div class="flex gap-x-6 items-center"> + <input + type="text" + placeholder="Type here" + name="override[]" + value=config_value.to_string() + class="input input-bordered w-full max-w-xs" + /> + <button + class="text-error text-xl font-light font-thin" + on:click=move |_| { + set_overrides + .update(|value| { value.remove(&config_key); }); - }} - > - <i class="ri-delete-bin-2-line"></i> - </button> - </div> + } + > + + <i class="ri-delete-bin-2-line"></i> + </button> </div> </div> + </div> </div> } } /> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/pagination/pagination.rs b/crates/frontend/src/components/pagination/pagination.rs index 1f119c594..cbf0281b8 100644 --- a/crates/frontend/src/components/pagination/pagination.rs +++ b/crates/frontend/src/components/pagination/pagination.rs @@ -13,19 +13,13 @@ where { view! { <div class="join"> - <button - class="join-item btn" - on:click=move |_| previous() - > + <button class="join-item btn" on:click=move |_| previous()> "«" </button> <button class="join-item btn"> - {format!("Page {} / {}", current_page, total_pages)} + {format!("Page {} / {}", current_page, total_pages)} </button> - <button - class="join-item btn" - on:click=move |_| next() - > + <button class="join-item btn" on:click=move |_| next()> "»" </button> </div> diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index ec84ab2a0..4304bdcee 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -1,42 +1,14 @@ use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; -use leptos::*; -use leptos_router::{use_location, A}; +use leptos::{logging::log, *}; +use leptos_router::{use_location, use_navigate, A}; +#[component] pub fn SideNav() -> impl IntoView { let location = use_location(); - let (app_routes, set_app_routes) = create_signal(vec![ - AppRoute { - key: "/admin/experiments".to_string(), - path: "/admin/experiments".to_string(), - icon: "ri-test-tube-fill".to_string(), - label: "Experiments".to_string(), - }, - AppRoute { - key: "/admin/dimensions".to_string(), - path: "/admin/dimensions".to_string(), - icon: "ri-ruler-2-fill".to_string(), - label: "Dimensions".to_string(), - }, - AppRoute { - key: "/admin/default-config".to_string(), - path: "/admin/default-config".to_string(), - icon: "ri-tools-line".to_string(), - label: "Default Config".to_string(), - }, - AppRoute { - key: "/admin/overrides".to_string(), - path: "/admin/overrides".to_string(), - icon: "ri-guide-fill".to_string(), - label: "Overrides".to_string(), - }, - AppRoute { - key: "/admin/resolve".to_string(), - path: "/admin/resolve".to_string(), - icon: "ri-equalizer-fill".to_string(), - label: "Resolve".to_string(), - }, - ]); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); + let (app_routes, set_app_routes) = create_signal(create_routes(&tenant_rs.get())); create_effect(move |_| { let current_path = location.pathname.get(); @@ -61,6 +33,37 @@ pub fn SideNav() -> impl IntoView { </span> </A> </div> + <select + value=tenant_rs.get() + on:change=move |change| { + let new_tenant = event_target_value(&change); + let location = use_location(); + let mut path_tokens = location + .pathname + .get() + .split("/") + .into_iter() + .map(ToString::to_string) + .collect::<Vec<String>>(); + log!("{}{:?}", new_tenant, path_tokens); + path_tokens.remove(0); + path_tokens.remove(0); + path_tokens.remove(0); + log!("{}{:?}", new_tenant, path_tokens); + let nav = use_navigate(); + set_app_routes.set(create_routes(&new_tenant)); + tenant_ws.set(new_tenant.clone()); + nav( + format!("admin/{new_tenant}/{}", path_tokens.join("/")).as_str(), + Default::default(), + ); + } + + class="select w-full max-w-xs" + > + <option selected>mjos</option> + <option>sdk_config</option> + </select> <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> <ul class="menu"> @@ -73,17 +76,53 @@ pub fn SideNav() -> impl IntoView { view! { <li class="mt-1 w-full"> <NavItem - href={route.path.to_string()} - icon={route.icon.to_string()} - text={route.label.to_string()} - is_active={is_active} + href=route.path.to_string() + icon=route.icon.to_string() + text=route.label.to_string() + is_active=is_active /> </li> } } /> + </ul> </div> </div> } } + +fn create_routes(tenant: &String) -> Vec<AppRoute> { + vec![ + AppRoute { + key: format!("admin/{}/experiments", tenant), + path: format!("admin/{}/experiments", tenant), + icon: "ri-test-tube-fill".to_string(), + label: "Experiments".to_string(), + }, + AppRoute { + key: format!("admin/{}/dimensions", tenant), + path: format!("admin/{}/dimensions", tenant), + icon: "ri-ruler-2-fill".to_string(), + label: "Dimensions".to_string(), + }, + AppRoute { + key: format!("admin/{}/default-config", tenant), + path: format!("admin/{}/default-config", tenant), + icon: "ri-tools-line".to_string(), + label: "Default Config".to_string(), + }, + AppRoute { + key: format!("admin/{}/overrides", tenant), + path: format!("admin/{}/overrides", tenant), + icon: "ri-guide-fill".to_string(), + label: "Overrides".to_string(), + }, + AppRoute { + key: format!("admin/{}/resolve", tenant), + path: format!("admin/{}/resolve", tenant), + icon: "ri-equalizer-fill".to_string(), + label: "Resolve".to_string(), + }, + ] +} diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index 0b83162c4..d3fd5caf3 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -1,5 +1,8 @@ +use crate::components::table::types::TableSettings; + use super::types::Column; use leptos::*; +use leptos_router::use_navigate; use serde_json::{json, Map, Value}; fn generate_table_row_str(row: &Value) -> String { @@ -19,57 +22,82 @@ fn generate_table_row_str(row: &Value) -> String { #[component] pub fn Table( - key_column: String, - table_style: String, + _key_column: String, + _table_style: String, columns: Vec<Column>, rows: Vec<Map<String, Value>>, + settings: TableSettings, ) -> impl IntoView { + let (redirect_info, _) = create_signal(settings.redirect_prefix); view! { <div class="overflow-x-auto"> <table class="table table-zebra"> <thead> <tr> <th></th> - { - columns - .iter() - .filter(|column| !column.hidden) - .map(|column| view! { - <th class="uppercase">{&column.name}</th> - }) - .collect_view() - } + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| view! { <th class="uppercase">{&column.name}</th> }) + .collect_view()} + </tr> </thead> <tbody> - { - rows - .iter() - .enumerate() - .map(|(index, row)| view! { - <tr> - <th>{index + 1}</th> - { - columns - .iter() - .filter(|column| !column.hidden) - .map(|column| { - let cname = &column.name; - let value: String = generate_table_row_str( - row - .get(cname) - .unwrap_or(&Value::String("".to_string())) - ); - view! { - <td>{(column.formatter)(&value, row)}</td> - } - }) - .collect_view() + + {rows + .iter() + .enumerate() + .map(|(index, row)| { + let row_id = row + .get("id") + .unwrap_or(&json!("")) + .as_str() + .unwrap() + .to_string(); + view! { + <tr + on:click=move |_| { + let redirect_info = redirect_info.get(); + if redirect_info.is_some() { + let prefix = redirect_info.unwrap(); + let nav = use_navigate(); + nav( + format!("{prefix}/{row_id}").as_str(), + Default::default(), + ); + } } + + style:cursor=move || { + if redirect_info.get().is_some() { + "pointer" + } else { + "default" + } + } + > + + <th>{index + 1}</th> + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + let cname = &column.name; + let value: String = generate_table_row_str( + row.get(cname).unwrap_or(&Value::String("".to_string())), + ); + view! { <td>{(column.formatter)(&value, &row)}</td> } + }) + .collect_view()} + </tr> - }) - .collect_view() - } + } + }) + .collect_view()} + </tbody> </table> </div> diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index c941ddb59..d5dc4478d 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -3,6 +3,11 @@ use serde_json::{Map, Value}; pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; +#[derive(Clone, Debug)] +pub struct TableSettings { + pub redirect_prefix: Option<String>, +} + #[derive(Clone, PartialEq)] pub struct Column { pub name: String, @@ -10,11 +15,8 @@ pub struct Column { pub formatter: CellFormatter, } -fn default_formatter(value: &str, row: &Map<String, Value>) -> View { - view! { - <span>{value.to_string()}</span> - } - .into_view() +fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { + view! { <span>{value.to_string()}</span> }.into_view() } impl Column { diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 85408dd0d..0e1c3e45f 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -5,7 +5,7 @@ use leptos::*; pub fn Layout(children: Children) -> impl IntoView { view! { <div> - <SideNav /> + <SideNav/> <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200"> {children()} </main> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 6ad0dda59..c12097e77 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -2,6 +2,7 @@ use leptos::logging::*; use leptos::*; use serde_json::{json, Map, Value}; +use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; use crate::pages::Dimensions::helper::fetch_dimensions; @@ -29,28 +30,24 @@ pub fn Dimensions() -> impl IntoView { view! { <div class="p-8"> - <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> <div class="stats shadow"> <div class="stat"> <div class="stat-figure text-primary"> - <i class="ri-ruler-2-fill text-5xl" /> + <i class="ri-ruler-2-fill text-5xl"></i> </div> <div class="stat-title">Dimensions</div> - { - move || { - let value = dimensions.get(); - let total_items = match value { - Some(v) => v.len(), - _ => 0, - }; - view! { - <div class="stat-value"> - {total_items} - </div> - } - } - } + + {move || { + let value = dimensions.get(); + let total_items = match value { + Some(v) => v.len(), + _ => 0, + }; + view! { <div class="stat-value">{total_items}</div> } + }} + </div> </div> </div> @@ -59,37 +56,31 @@ pub fn Dimensions() -> impl IntoView { <div class="card-body"> <h2 class="card-title">Dimensions</h2> <div> - { - move || { - let value = dimensions.get(); - match value { - Some(v) => { - let data = v - .iter() - .map(|ele| { - json!(ele) - .as_object() - .unwrap() - .clone() - }) - .collect::<Vec<Map<String, Value>>>() - .to_owned(); - println!("hello1: {:?}", data); - view! { - <Table - table_style="abc".to_string() - rows={data} - key_column="id".to_string() - columns={table_columns.get()} - /> - } - }, - None => { - view! {<div>Loading....</div> }.into_view() + + {move || { + let value = dimensions.get(); + match value { + Some(v) => { + let data = v + .iter() + .map(|ele| { json!(ele).as_object().unwrap().clone() }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + println!("hello1: {:?}", data); + view! { + <Table + _table_style="abc".to_string() + rows=data + _key_column="id".to_string() + columns=table_columns.get() + settings={TableSettings {redirect_prefix: None}} + /> } } + None => view! { <div>Loading....</div> }.into_view(), } - } + }} + </div> </div> </div> diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 15e68bcc7..828e0bb96 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,9 +1,20 @@ use chrono::{DateTime, Utc}; -use leptos::*; +use leptos::{html::Input, logging::log, *}; use leptos_router::use_params_map; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Map, Value}; use tracing::debug; +use web_sys::SubmitEvent; + +use crate::components::{ + experiment_form::experiment_form::ExperimentForm, + table::{ + table::Table, + types::{Column, TableSettings}, + }, +}; + +use super::ExperimentList::utils::{fetch_default_config, fetch_dimensions}; #[derive( Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, @@ -41,17 +52,64 @@ pub struct Experiment { pub(crate) traffic_percentage: u8, pub(crate) context: Value, pub(crate) status: ExperimentStatusType, + pub(crate) override_keys: Value, pub(crate) created_by: String, pub(crate) created_at: DateTime<Utc>, pub(crate) last_modified: DateTime<Utc>, - pub(crate) chosen_variant: Option<Variant>, + pub(crate) chosen_variant: Option<String>, } -async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { +async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, String> { let client = reqwest::Client::new(); match client .get(format!("http://localhost:8080/experiments/{}", exp_id)) + .header("x-tenant", tenant) + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} + +async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .patch(format!("http://localhost:8080/experiments/{}/ramp", exp_id)) .header("x-tenant", "mjos") + .json(&json!({ "traffic_percentage": percent })) + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} + +async fn conclude_experiment( + exp_id: String, + variant_id: String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .patch(format!( + "http://localhost:8080/experiments/{}/conclude", + exp_id + )) + .header("x-tenant", "mjos") + .json(&json!({ "chosen_variant": variant_id })) .send() .await { @@ -69,136 +127,407 @@ async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { #[component] pub fn experiment_page() -> impl IntoView { let exp_params = use_params_map(); - let experiment_id = - move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); - let experiment_info = create_resource(experiment_id, |exp_id: String| async move { - get_experiment(&exp_id).await + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let source = move || { + let t = tenant_rs.get(); + let exp_id = + exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + (exp_id, t) + }; + + let experiment_info = create_resource(source, |(exp_id, tenant)| async move { + get_experiment(&exp_id, &tenant).await }); view! { - <Transition - fallback= move || view! {<h1> Loading.... </h1>} > - {move || - match experiment_info.get() { - Some(Ok(experiment)) => experiment_detail_view(&experiment).into_view(), - Some(Err(err)) => view! {<h1>{err.to_string()}</h1>}.into_view(), - None => view! {<h1>No elements </h1>}.into_view(), - } - } + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> + {move || match experiment_info.get() { + Some(Ok(experiment)) => { + experiment_detail_view(experiment, experiment_info).into_view() + } + Some(Err(err)) => view! { <h1>{err.to_string()}</h1> }.into_view(), + None => view! { <h1>No elements</h1> }.into_view(), + }} + </Transition> } } -fn experiment_detail_view(exp: &Experiment) -> impl IntoView { +fn experiment_detail_view( + initial_data: Experiment, + exp_resource: Resource<(String, String), Result<Experiment, String>>, +) -> impl IntoView { + let contexts = initial_data.context["and"] + .as_array() + .unwrap_or(&Vec::new()) + .to_owned(); + let (experiment, _) = create_signal(initial_data); + let (ctxs, _) = create_signal(contexts); + view! { <div class="flex flex-col overflow-x-auto p-2"> - <h1 class="text-4xl pt-4 font-extrabold"> - {&exp.name} - <span class="badge ml-3 mb-1 badge-primary badge-lg">{exp.status.to_string()}</span> - </h1> - - <div class="divider"></div> - - <div class="join m-5"> - <button class="btn join-item"><i class="ri-edit-line"></i>Edit</button> - <button class="btn join-item"><i class="ri-stop-circle-line"></i>Conclude</button> - <button class="btn join-item"><i class="ri-guide-line"></i>Release</button> - <button class="btn join-item"><i class="ri-flight-takeoff-line"></i>Ramp</button> - </div> + {move || { + experiment + .with(|exp| { + let class_name = match exp.status { + ExperimentStatusType::CREATED => { + "badge ml-3 mb-1 badge-lg badge-primary" + } + ExperimentStatusType::INPROGRESS => { + "badge ml-3 mb-1 badge-lg badge-warning" + } + ExperimentStatusType::CONCLUDED => { + "badge ml-3 mb-1 badge-lg badge-success" + } + }; + view! { + <h1 class="text-4xl pt-4 font-extrabold"> + {&exp.name} <span class=class_name>{exp.status.to_string()}</span> + </h1> + } + }) + }} + <div class="divider"></div> + <div class="flex flex-row justify-end join m-5"> + {move || { + experiment + .with(|exp| { + match exp.status { + ExperimentStatusType::CREATED => { + view! { + <button + class="btn join-item" + onclick="edit_exp_modal.showModal()" + > + <i class="ri-edit-line"></i> + Edit + </button> + <button + class="btn join-item" + value=&exp.id + on:click=move |button_event| spawn_local(async move { + let value = event_target_value(&button_event); + let _ = ramp_experiment(&value, 1).await; + exp_resource.refetch(); + }) + > - <div class="stats shadow-xl mt-5"> - <div class="stat"> - <div class="stat-title">Experiment ID</div> - <div class="stat-value">{&exp.id}</div> - </div> - <div class="stat"> - <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value">{exp.traffic_percentage}</div> - </div> - <div class="stat"> - <div class="stat-title">Created by</div> - <div class="stat-value">{&exp.created_by}</div> - </div> - <div class="stat"> - <div class="stat-title">Created at</div> - <div class="stat-value">{format!("{}", &exp.created_at.format("%d-%m-%Y %H:%M:%S"))}</div> - </div> - <div class="stat"> - <div class="stat-title">Last Modified</div> - <div class="stat-value">{format!("{}", &exp.last_modified.format("%d-%m-%Y %H:%M:%S"))}</div> - </div> - </div> + <i class="ri-guide-line"></i> + Start + </button> + } + } + ExperimentStatusType::INPROGRESS => { + view! { + // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> + <button + class="btn join-item" + onclick="conclude_exp_modal.showModal()" + > + <i class="ri-stop-circle-line"></i> + Conclude + </button> + <button + class="btn join-item" + onclick="ramp_exp_modal.showModal()" + > + <i class="ri-flight-takeoff-line"></i> + Ramp + </button> + } + } + ExperimentStatusType::CONCLUDED => { + view! { + // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> + + // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> + + // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> + <div></div> + <div class="stat"> + <div class="stat-title">Chosen Variant</div> + <div class="stat-value"> + {match exp.chosen_variant { + Some(ref v) => format!("{}", v), + None => String::new(), + }} - <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Context</h2> - <div class="flex flex-row"> - <div class="stat"> - <div class="stat-title">Client ID</div> - <div class="stat-value">cac</div> + </div> + </div> + } + } + } + }) + }} + + </div> <div class="stats shadow-xl mt-5"> + <div class="stat"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value">{experiment.get().id}</div> + </div> + <div class="stat"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value">{move || experiment.get().traffic_percentage}</div> + </div> + <div class="stat"> + <div class="stat-title">Created by</div> + <div class="stat-value">{experiment.get().created_by}</div> + </div> + <div class="stat"> + <div class="stat-title">Created at</div> + <div class="stat-value"> + {format!("{}", experiment.get().created_at.format("%v"))} </div> - <div class="divider divider-horizontal">&&</div> - <div class="stat"> - <div class="stat-title">OS</div> - <div class="stat-value">android</div> + </div> + <div class="stat"> + <div class="stat-title">Last Modified</div> + <div class="stat-value"> + {move || { + experiment.with(|exp| format!("{}", &exp.last_modified.format("%v"))) + }} + </div> </div> - </div> - </div> + </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row"> + {move || { + let contexts = move || ctxs.get().into_iter(); + let mut view: Vec<_> = Vec::new(); + for item in contexts() { + for (_, value) in item.as_object().unwrap().into_iter() { + let rule_vector = value.as_array().unwrap(); + let mut rule_iter = rule_vector.to_owned().into_iter(); + let (var, value) = ( + rule_iter.next().unwrap(), + rule_iter.next().unwrap(), + ); + let dimension = var.as_object().unwrap().get("var").unwrap(); + view.push( + view! { + <div class="stat"> + <div class="stat-title"> + {format!("{}", dimension.as_str().unwrap())} + </div> + <div class="stat-value"> + {format!("{}", value.as_str().unwrap())} + </div> + </div> + }, + ) + } + } + view + }} + </div> + </div> + </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto overflow-y-auto"> + {move || { + let exp = move || experiment.get(); + let rows = gen_variant_rows(&exp().variants).unwrap(); + let mut columns: Vec<Column> = Vec::new(); + let settings = TableSettings { + redirect_prefix: None, + }; + columns.push(Column::default("Variant".into())); + for okey in exp().override_keys.as_array().unwrap().into_iter() { + columns.push(Column::default(okey.as_str().unwrap().into())); + } + view! { + <Table + _table_style="abc".to_string() + rows=rows + _key_column="overrides".to_string() + columns=columns + settings=settings + /> + } + }} - <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Variants</h2> - <div class="overflow-x-auto"> - <table class="table"> - <thead> - <tr class="bg-base-200"> - <th></th> - <th>Key</th> - <th>Variant-1</th> - <th>Variant-2</th> - <th>Variant-3</th> - <th>Variant-4</th> - <th>Variant-5</th> - <th>Control</th> - </tr> - </thead> - <tbody> - <tr> - <th>1</th> - <td>pmTestKey1</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Blue</td> - <td>Blue</td> - </tr> - <tr> - <th>2</th> - <td>pmTestKey2</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Purple</td> - </tr> - <tr> - <th>3</th> - <td>pmTestKey3</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Red</td> - </tr> - </tbody> - </table> + </div> </div> </div> </div> - </div> + {add_dialogs(experiment, exp_resource)} + } +} + +fn gen_variant_rows(variants: &Vec<Variant>) -> Result<Vec<Map<String, Value>>, String> { + let mut rows: Vec<Map<String, Value>> = Vec::new(); + for (i, variant) in variants.into_iter().enumerate() { + let variant_name = match variant.variant_type { + VariantType::CONTROL => "Control".into(), + VariantType::EXPERIMENTAL => format!("Variant-{i}"), + }; + let mut m = Map::new(); + m.insert("Variant".into(), variant_name.into()); + m.insert("variant_id".into(), variant.id.clone().into()); + for (o, value) in variant.overrides.as_object().unwrap().into_iter() { + m.insert(o.clone(), value.clone()); + } + rows.push(m); + } + Ok(rows) +} + +fn add_dialogs( + experiment_rs: ReadSignal<Experiment>, + experiment_ws: Resource<(String, String), Result<Experiment, String>>, +) -> impl IntoView { + let input_element: NodeRef<Input> = create_node_ref(); + let experiment = move || experiment_rs.get(); + let (traffic, set_traffic) = create_signal(experiment().traffic_percentage); + + let on_submit = move |ev: SubmitEvent| { + ev.prevent_default(); + let value = input_element + .get() + .expect("<input> to exist") + .value_as_number() as u8; + spawn_local(async move { + let _ = ramp_experiment(&experiment().id, value).await; + experiment_ws.refetch(); + }); + }; + + let dimensions = create_resource( + || (), + |_| async move { + match fetch_dimensions().await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let default_config = create_resource( + || (), + |_| async move { + match fetch_default_config().await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + match experiment_rs.get().status { + ExperimentStatusType::CREATED => view! { + <dialog id="edit_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Edit Experiment</h3> + <div class="modal-action"> + <ExperimentForm + name=experiment_rs.get().name + context=vec![] + variants=vec![] + dimensions=dimensions.get().unwrap_or(vec![]) + default_config=default_config.get().unwrap_or(vec![]) + /> + </div> + </div> + </dialog> + } + .into_view(), + ExperimentStatusType::INPROGRESS => view! { + <dialog id="conclude_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Conclude This Experiment</h3> + <p class="py-4"> + Choose a variant to conclude with, this variant becomes + the new default that is served to requests that match this context + </p> + <form method="dialog"> + {move || { + let mut view_arr = vec![]; + for (i, v) in experiment_rs.get().variants.into_iter().enumerate() { + let (variant, _) = create_signal(v); + let view = match variant.get().variant_type { + VariantType::CONTROL => { + view! { + <button + class="btn btn-block btn-outline btn-success m-2" + on:click=move |_| spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone()) + .await + .unwrap(); + experiment_ws.refetch(); + }) + > + + Control + </button> + } + } + VariantType::EXPERIMENTAL => { + view! { + <button + class="btn btn-block btn-outline btn-info m-2" + on:click=move |_| spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone()) + .await + .unwrap(); + experiment_ws.refetch(); + }) + > + + {format!("Variant-{i}")} + </button> + } + } + }; + view_arr.push(view); + } + view_arr + }} + + </form> + <div class="modal-action"> + <form method="dialog"> + <button class="btn">Close</button> + </form> + </div> + </div> + </dialog> + <dialog id="ramp_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Ramp up with release</h3> + <p class="py-4">Increase the traffic being redirected to the variants</p> + <form method="dialog" on:submit=on_submit> + <p>{move || traffic.get()}</p> + <input + type="range" + min="0" + max="100" + node_ref=input_element + value=move || experiment_rs.get().traffic_percentage + class="range" + on:input=move |et| { + let t = event_target_value(&et).parse::<u8>().unwrap(); + log!("traffic value:{t}"); + set_traffic.set(t); + } + /> + + <button class="btn btn-block btn-outline btn-success m-2">Set</button> + </form> + <div class="modal-action"> + <form method="dialog"> + <button class="btn">Close</button> + </form> + </div> + </div> + </dialog> + }.into_view(), + ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), } } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 6b73bee4d..d37e1de26 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,21 +1,18 @@ use leptos::logging::*; use leptos::*; -use leptos_router::*; -use chrono::{ - prelude::{DateTime, Utc}, - TimeZone, -}; +use chrono::{prelude::Utc, TimeZone}; use crate::components::{ + experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, - table::{table::Table, types::Column}, - experiment_form::experiment_form::ExperimentForm + table::{ + table::Table, + types::{Column, TableSettings}, + }, }; -use crate::pages::ExperimentList::types::{ - ExperimentResponse, ExperimentsResponse, ListFilters, -}; +use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; use super::utils::{fetch_default_config, fetch_dimensions, fetch_experiments}; use serde_json::{json, Map, Value}; @@ -23,13 +20,13 @@ use serde_json::{json, Map, Value}; #[component] pub fn ExperimentList() -> impl IntoView { // acquire tenant - let tenant = "test".to_string(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (filters, set_filters) = create_signal(ListFilters { status: None, from_date: Utc.timestamp_opt(0, 0).single(), to_date: Utc.timestamp_opt(4130561031, 0).single(), page: Some(1), - count: Some(1), + count: Some(10), }); let table_columns = create_memo(move |_| { @@ -62,11 +59,11 @@ pub fn ExperimentList() -> impl IntoView { }); let experiments = create_blocking_resource( - move || filters.get(), - |value| async move { - match fetch_experiments(value).await { + move || (tenant_rs.get(), filters.get()), + |(current_tenant, value)| async move { + match fetch_experiments(value, ¤t_tenant).await { Ok(data) => data, - Err(e) => ExperimentsResponse { + Err(_) => ExperimentsResponse { total_items: 0, total_pages: 0, data: vec![], @@ -80,7 +77,7 @@ pub fn ExperimentList() -> impl IntoView { |_| async move { match fetch_dimensions().await { Ok(data) => data, - Err(e) => vec![], + Err(_) => vec![], } }, ); @@ -90,7 +87,7 @@ pub fn ExperimentList() -> impl IntoView { |_| async move { match fetch_default_config().await { Ok(data) => data, - Err(e) => vec![], + Err(_) => vec![], } }, ); @@ -98,28 +95,27 @@ pub fn ExperimentList() -> impl IntoView { // TODO: Add filters view! { <div class="p-8"> - <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> <div class="stats shadow"> <div class="stat"> <div class="stat-figure text-primary"> - <i class="ri-test-tube-fill text-5xl" /> + <i class="ri-test-tube-fill text-5xl"></i> </div> <div class="stat-title">Experiments</div> - { - move || { - let value = experiments.get(); - let total_items = match value { - Some(v) => v.total_items, - _ => 0, - }; - view! { - <div class="stat-value"> - {total_items} - </div> - } - } - } + + {move || { + let value = experiments.get(); + let total_items = match value { + Some(v) => v.total_items, + _ => 0, + }; + view! { <div class="stat-value">{total_items}</div> } + }} + + </div> + <div class="stat cursor-pointer" onclick="create_exp_modal.showModal()"> + <div class="stat-figure text-primary">new</div> </div> </div> </div> @@ -127,97 +123,103 @@ pub fn ExperimentList() -> impl IntoView { <div class="card-body"> <h2 class="card-title">Experiments</h2> <div> - { - move || { - let value = experiments.get(); - match value { - Some(v) => { - let data = v - .data - .iter() - .map(|ele| { - json!(ele) - .as_object() - .unwrap() - .clone() - }) - .collect::<Vec<Map<String, Value>>>() - .to_owned(); - view! { - <Table - table_style="abc".to_string() - rows={data} - key_column="id".to_string() - columns={table_columns.get()} - /> - } - }, - None => { - view! {<div>Loading....</div> }.into_view() + + {move || { + let current_tenant = tenant_rs.get(); + let settings = TableSettings { + redirect_prefix: Some( + format!("admin/{current_tenant}/experiments"), + ), + }; + let value = experiments.get(); + match value { + Some(v) => { + let data = v + .data + .iter() + .map(|ele| { json!(ele).as_object().unwrap().clone() }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + <Table + _table_style="abc".to_string() + rows=data + _key_column="id".to_string() + columns=table_columns.get() + settings=settings + /> } } + None => view! { <div>Loading....</div> }.into_view(), } + }} - } </div> <div class="mt-2 flex justify-end"> - { - move || { - let current_page = filters.get().page.unwrap_or(0); - let total_pages = match experiments.get() { - Some(val) => val.total_pages, - None => 0, - }; - - view! { - <Pagination - current_page={current_page} - total_pages={total_pages} - next={ - move || { - set_filters.update(|f| { - f.page = match f.page { - Some(p) if p < total_pages => Some(p + 1), - Some(p) => Some(p), - None => None, - } - }); - } - } - previous={ - move || { - set_filters.update(|f| { - f.page = match f.page { - Some(p) if p > 1 => Some(p - 1), - Some(p) => Some(p), - None => None, - } - }); - } - } - /> - } + + {move || { + let current_page = filters.get().page.unwrap_or(0); + let total_pages = match experiments.get() { + Some(val) => val.total_pages, + None => 0, + }; + view! { + <Pagination + current_page=current_page + total_pages=total_pages + next=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p < total_pages => Some(p + 1), + Some(p) => Some(p), + None => None, + } + }); + } + + previous=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p > 1 => Some(p - 1), + Some(p) => Some(p), + None => None, + } + }); + } + /> } - } + }} + </div> </div> </div> - { - move || { - let dim = dimensions.get().unwrap_or(vec![]); - let def_conf = default_config.get().unwrap_or(vec![]); - view! { - <ExperimentForm - name="".to_string() - context=vec![] - variants=vec![] - dimensions={dim} - default_config={def_conf} - /> - } + + {move || { + let dim = dimensions.get().unwrap_or(vec![]); + let def_conf = default_config.get().unwrap_or(vec![]); + view! { + <dialog id="create_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Create an Experiment</h3> + <div class="modal-action flex flex-col"> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim + default_config=def_conf + /> + </div> + </div> + </dialog> } - } + }} + </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index b0804c91b..58b80995a 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -81,4 +81,4 @@ pub struct Variant { pub context_id: Option<String>, pub override_id: Option<String>, pub overrides: Map<String, Value>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 74d4592ab..38e19fbc4 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,8 +1,9 @@ -use super::types::{ExperimentsResponse, ListFilters, Dimension, DefaultConfig}; +use super::types::{DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; use std::vec::Vec; pub async fn fetch_experiments( filters: ListFilters, + tenant: &String, ) -> Result<ExperimentsResponse, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { @@ -34,7 +35,7 @@ pub async fn fetch_experiments( let url = format!("{}/experiments?{}", host, query_params.join("&")); let response: ExperimentsResponse = client .get(url) - .header("x-tenant", "mjos") + .header("x-tenant", tenant) .send() .await .map_err(|e| e.to_string())? @@ -45,7 +46,6 @@ pub async fn fetch_experiments( Ok(response) } - pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { @@ -70,7 +70,6 @@ pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { Ok(response) } - pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { @@ -93,4 +92,4 @@ pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { .map_err(|e| e.to_string())?; Ok(response) -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 8527a501d..dc58cd547 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -112,101 +112,111 @@ pub fn Home() -> impl IntoView { create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); view! { - <div class="container mt-5" > - <div class="text-center mb-4"> - <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> - </div> - <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> - { - config_data.with(move |result| { - match result { - Some(Ok(config)) => { - let rows = |k:&String, v:&Value| { - let key = k.replace("\"", "").trim().to_string(); - let value = format!("{}", v).replace("\"", "").trim().to_string(); - view! { - <tr> - <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> - <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> - </tr> - } - }; - - let contexts_views: Vec<_> = config.contexts.iter().map(|context| { - let condition = extract_and_format(&context.condition); - let rows: Vec<_> = context.override_with_keys.iter() - .filter_map(|key| config.overrides.get(key)) - .flat_map(|ovr| ovr.as_object().unwrap().iter()) - .map(|(k, v)| { - rows(&k,&v) - }).collect(); - - view! { - <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> - <table class="table table-responsive table-bordered table-hover border-secondary"> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody class="bg-light"> - { rows } - </tbody> - </table> - } - }).collect::<Vec<_>>(); - - let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); - let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ - rows(&k,&v) - }).collect(); - - vec![ - view! { - <div class="mb-4 "> - { new_context_views } - <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> - <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody> - {default_config} - </tbody> - </table> - </div> - } - ] - }, - Some(Err(error)) => { - vec![ - view! { - <div class="error"> - {"Failed to fetch config data: "} - {error} - </div> - } - ] - }, - None => { - vec![ - view! { - <div class="error"> - {"No config data fetched"} - </div> - } - ] - } - } - }) - } - </Suspense> - </div> - - + <div class="container mt-5"> + <div class="text-center mb-4"> + <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> + </div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let rows = |k: &String, v: &Value| { + let key = k.replace("\"", "").trim().to_string(); + let value = format!("{}", v) + .replace("\"", "") + .trim() + .to_string(); + view! { + <tr> + <td class="fw-normal col w-50 shadow-sm"> + <div class="col">{key}</div> + </td> + <td class="fw-normal col w-50 shadow-sm"> + <div class="col">{value}</div> + </td> + </tr> + } + }; + let contexts_views: Vec<_> = config + .contexts + .iter() + .map(|context| { + let condition = extract_and_format(&context.condition); + let rows: Vec<_> = context + .override_with_keys + .iter() + .filter_map(|key| config.overrides.get(key)) + .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .map(|(k, v)| { rows(&k, &v) }) + .collect(); + view! { + <h6 class="fw-normal font-monospace"> + "Condition: " + <span class="badge rounded-pill bg-secondary small"> + {&condition} + </span> + </h6> + <table class="table table-responsive table-bordered table-hover border-secondary"> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody class="bg-light">{rows}</tbody> + </table> + } + }) + .collect::<Vec<_>>(); + let new_context_views = contexts_views + .into_iter() + .rev() + .collect::<Vec<_>>(); + let default_config: Vec<_> = config + .default_configs + .iter() + .map(|(k, v)| { rows(&k, &v) }) + .collect(); + vec![ + view! { + <div class="mb-4 "> + {new_context_views} + <h6 class="mb-3 f-6 fw-normal font-monospace"> + "Default Configuration" + </h6> + <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{default_config}</tbody> + </table> + </div> + }, + ] + } + Some(Err(error)) => { + vec![ + view! { + <div class="error"> + {"Failed to fetch config data: "} {error} + </div> + }, + ] + } + None => { + vec![view! { <div class="error">{"No config data fetched"}</div> }] + } + } + })} + + </Suspense> + </div> } } diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs index 52b56389c..d50db7d11 100644 --- a/crates/frontend/src/pages/NotFound/NotFound.rs +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -15,7 +15,5 @@ pub fn NotFound() -> impl IntoView { let resp = expect_context::<leptos_actix::ResponseOptions>(); resp.set_status(actix_web::http::StatusCode::NOT_FOUND); } - view! { - <h1>"Not Found"</h1> - } + view! { <h1>"Not Found"</h1> } } diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index 02769d434..4e2fb6969 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -1,4 +1,4 @@ -use derive_more::{Deref, DerefMut}; +use leptos::{ReadSignal, WriteSignal}; use std::vec::Vec; #[derive(Clone, Debug)] @@ -8,3 +8,5 @@ pub struct AppRoute { pub icon: String, pub label: String, } + +pub type InputVector = Vec<(ReadSignal<String>, WriteSignal<String>)>; diff --git a/flake.nix b/flake.nix index 76dbaeead..d83cb7597 100644 --- a/flake.nix +++ b/flake.nix @@ -53,6 +53,7 @@ jq nodejs_18 wasm-pack + leptosfmt ( rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; targets = [ "wasm32-unknown-unknown" ]; From c8bcfcc06a573a817e8d37570414abd4c026ff3e Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Fri, 24 Nov 2023 16:39:33 +0530 Subject: [PATCH 202/352] feat: added default config page --- .env.example | 4 +- crates/frontend/src/app.rs | 6 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 261 ++++++++++++++++++ .../src/pages/DefaultConfig/helper.rs | 67 +++++ .../frontend/src/pages/DefaultConfig/mod.rs | 3 + .../frontend/src/pages/DefaultConfig/types.rs | 16 ++ crates/frontend/src/pages/mod.rs | 1 + 7 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs create mode 100644 crates/frontend/src/pages/DefaultConfig/helper.rs create mode 100644 crates/frontend/src/pages/DefaultConfig/mod.rs create mode 100644 crates/frontend/src/pages/DefaultConfig/types.rs diff --git a/.env.example b/.env.example index 67b0ed5a3..81bcbfc32 100644 --- a/.env.example +++ b/.env.example @@ -20,9 +20,9 @@ MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 DASHBOARD_AUTH_ENABLED=false -ENABLE_TENANT_AND_SCOPE=false +ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/,/default-config" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/,/default-config,/pkg/style.css,/assets" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 3b7a2c669..49a188bad 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -6,7 +6,8 @@ use crate::hoc::layout::layout::Layout; use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::ExperimentList::ExperimentList::ExperimentList; use crate::pages::{ - Experiment::ExperimentPage, Home::Home::Home, NotFound::NotFound::NotFound, + DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, + Home::Home::Home, NotFound::NotFound::NotFound, }; #[component] @@ -33,7 +34,7 @@ pub fn App() -> impl IntoView { <Routes> <Route ssr=SsrMode::PartiallyBlocked - path="/admin/dimensions" + path="/admin/:tenant/dimensions" view=Dimensions /> <Route @@ -47,6 +48,7 @@ pub fn App() -> impl IntoView { view=ExperimentPage /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> + <Route ssr=SsrMode::PartiallyBlocked path="/admin/default-config" view=DefaultConfig/> <Route path="/*any" view=NotFound/> </Routes> </Layout> diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs new file mode 100644 index 000000000..7282d1020 --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -0,0 +1,261 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use crate::components::table::{table::Table, types::Column}; +use crate::pages::DefaultConfig::types::Config; +use leptos::ev::SubmitEvent; +use leptos::spawn_local; +use leptos::*; +use leptos_router::use_query_map; +use serde_json::{Map, Value}; + +pub async fn fetch_config(tenant: String) -> Result<Config, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + let url = format!("{host}/config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +pub async fn create_default_config( + tenant: String, + key: String, + value: String, + key_type: String, + pattern: String, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = match std::env::var("APP_ENV").as_deref() { + Ok("PROD") => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", + _ => "http://localhost:8080", + }; + let url = format!("{host}/default-config/{key}"); + let mut req_body: HashMap<&str, Value> = HashMap::new(); + let mut schema: Map<String, Value> = Map::new(); + schema.insert("type".to_string(), Value::String(key_type)); + schema.insert("pattern".to_string(), Value::String(pattern)); + req_body.insert("value", Value::String(value)); + req_body.insert("schema", Value::Object(schema)); + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&req_body) + .send() + .await + .map_err(|e| e.to_string())?; + response.text().await.map_err(|e| e.to_string()) +} + +#[component] +fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { + view! { + <div class="p-6 bg-white text-gray-600"> + <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> + Create DefaultConfig + <i class="ri-edit-2-line ml-2"></i> + </button> + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white"> + <FormComponent handle_submit = handle_submit/> + </div> + </dialog> + </div> + } +} + +#[component] +fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { + use leptos::html::Input; + let handle_submit = handle_submit.clone(); + + let (key, set_key) = create_signal("key1".to_string()); + let (value, set_value) = create_signal("value1".to_string()); + let (keytype, set_keytype) = create_signal("string".to_string()); + let (pattern, set_pattern) = create_signal(".*".to_string()); + + let input_element: NodeRef<Input> = create_node_ref(); + let input_element_two: NodeRef<Input> = create_node_ref(); + let input_element_three: NodeRef<Input> = create_node_ref(); + let input_element_four: NodeRef<Input> = create_node_ref(); + + let on_submit = { + let handle_submit = handle_submit.clone(); + move |ev: SubmitEvent| { + ev.prevent_default(); + + let value1 = input_element.get().expect("<input> to exist").value(); + let value2 = input_element_two.get().expect("<input> to exist").value(); + let value3 = input_element_three.get().expect("<input> to exist").value(); + let value4 = input_element_four.get().expect("<input> to exist").value(); + + set_key.set(value1.clone()); + set_value.set(value2.clone()); + set_keytype.set(value3.clone()); + set_pattern.set(value4.clone()); + let handle_submit_clone = handle_submit.clone(); + + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_default_config( + "mjos".to_string(), + key.get(), + value.get(), + keytype.get(), + pattern.get(), + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(_) => { + // Handle error + // Consider logging or displaying the error + } + } + } + }); + } + }; + + view! { + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Key</span> + </label> + <input type="text" placeholder="Key" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=key + node_ref=input_element + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input type="text" placeholder="Value" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=value + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Type</span> + </label> + <input type="text" placeholder="Type" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=keytype + node_ref=input_element_three + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> + </label> + <input type="text" placeholder="Pattern" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=pattern + node_ref=input_element_four + /> + </div> + <div class="form-control mt-6"> + <button type="submit" class="btn btn-primary shadow-md font-mono" onclick="my_modal_5.close()">Submit</button> + </div> + </form> + } +} + +#[component] +pub fn DefaultConfig() -> impl IntoView { + let query = use_query_map(); + + let tenant = query.with(|params_map| { + params_map + .get("tenant") + .cloned() + .unwrap_or_else(|| "mjos".to_string()) + }); + + let config_data = + create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); + + let table_columns = create_memo(move |_| { + vec![ + Column::default("KEY".to_string()), + Column::default("VALUE".to_string()), + ] + }); + + view! { + <div class="p-8"> + <ModalComponent handle_submit = Rc::new(move || config_data.refetch()) /> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> + { + move || config_data.with( move |result| { + match result { + Some(Ok(config)) => { + let mut default_config: Vec<Map<String, Value>> = Vec::new(); + + for (key, value) in config.default_configs.iter() { + let mut map = Map::new(); + + let trimmed_key = Value::String(key.trim_matches('"').to_string()); + let formatted_value = Value::String(format!("{}", value).trim_matches('"').to_string()); + + map.insert("KEY".to_string(), trimmed_key); + map.insert("VALUE".to_string(), formatted_value); + default_config.push(map); + } + + vec![ + view! { + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <h2 class="card-title">Default Config</h2> + <Table + table_style="hover".to_string() + rows={default_config} + key_column="id".to_string() + columns={table_columns.get()} + /> + </div> + + </div> + } + ] + }, + Some(Err(error)) => { + vec![ + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} + {error} + </div> + } + ] + }, + None => { vec![view! {<div>Loading....</div> }]}, + } + }) + } + </Suspense> + </div> + + + } +} diff --git a/crates/frontend/src/pages/DefaultConfig/helper.rs b/crates/frontend/src/pages/DefaultConfig/helper.rs new file mode 100644 index 000000000..f0cfafa78 --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/helper.rs @@ -0,0 +1,67 @@ +use serde_json::Value; + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} diff --git a/crates/frontend/src/pages/DefaultConfig/mod.rs b/crates/frontend/src/pages/DefaultConfig/mod.rs new file mode 100644 index 000000000..53d091161 --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/mod.rs @@ -0,0 +1,3 @@ +pub mod DefaultConfig; +pub mod helper; +pub mod types; diff --git a/crates/frontend/src/pages/DefaultConfig/types.rs b/crates/frontend/src/pages/DefaultConfig/types.rs new file mode 100644 index 000000000..5f04ae551 --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/types.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index d3257d205..996a2eb0f 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,3 +1,4 @@ +pub mod DefaultConfig; pub mod Dimensions; pub mod Experiment; pub mod ExperimentList; From 27f1236b508ddd4d0e297297748eda5a36feff59 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 30 Nov 2023 15:16:24 +0530 Subject: [PATCH 203/352] feat: added default config and override screen --- .env.example | 4 +- Cargo.lock | 25 +- crates/frontend/Cargo.toml | 3 +- crates/frontend/src/app.rs | 10 +- .../components/context_form/context_form.rs | 16 +- .../experiment_form/experiment_form.rs | 2 - .../components/override_form/override_form.rs | 114 ++--- crates/frontend/src/components/table/table.rs | 9 +- crates/frontend/src/components/table/types.rs | 9 +- .../pages/ContextOverride/ContextOverride.rs | 455 ++++++++++++++++++ .../frontend/src/pages/ContextOverride/mod.rs | 1 + .../src/pages/DefaultConfig/DefaultConfig.rs | 215 ++++++--- .../src/pages/Dimensions/Dimensions.rs | 5 +- .../pages/ExperimentList/ExperimentList.rs | 40 +- crates/frontend/src/pages/mod.rs | 1 + flake.nix | 1 + 16 files changed, 715 insertions(+), 195 deletions(-) create mode 100644 crates/frontend/src/pages/ContextOverride/ContextOverride.rs create mode 100644 crates/frontend/src/pages/ContextOverride/mod.rs diff --git a/.env.example b/.env.example index 81bcbfc32..d399d4ffa 100644 --- a/.env.example +++ b/.env.example @@ -20,9 +20,9 @@ MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 DASHBOARD_AUTH_ENABLED=false -ENABLE_TENANT_AND_SCOPE=true +ENABLE_TENANT_AND_SCOPE=false TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/,/default-config,/pkg/style.css,/assets" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" diff --git a/Cargo.lock b/Cargo.lock index ba6d6c1b5..56fb7a1ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1437,6 +1437,7 @@ dependencies = [ "derive_more", "futures", "http", + "js-sys", "leptos", "leptos_actix", "leptos_meta", @@ -1954,9 +1955,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -4162,9 +4163,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4172,9 +4173,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -4199,9 +4200,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4209,9 +4210,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -4222,9 +4223,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 2a281466e..57dae92f5 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -16,7 +16,7 @@ leptos = { version = "0.5.2" } leptos_meta = { version = "0.5.2" } leptos_actix = { version = "0.5.2", optional = true } leptos_router = { version = "0.5.2" } -wasm-bindgen = "=0.2.87" +wasm-bindgen = "=0.2.89" reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -27,6 +27,7 @@ anyhow = { workspace = true } chrono = {workspace = true } strum_macros = { workspace = true } strum = { workspace = true } +js-sys = "0.3.65" [features] diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 49a188bad..ba319694f 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -6,10 +6,10 @@ use crate::hoc::layout::layout::Layout; use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::ExperimentList::ExperimentList::ExperimentList; use crate::pages::{ + ContextOverride::ContextOverride::ContextOverride, DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, Home::Home::Home, NotFound::NotFound::NotFound, }; - #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. @@ -48,8 +48,14 @@ pub fn App() -> impl IntoView { view=ExperimentPage /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> - <Route ssr=SsrMode::PartiallyBlocked path="/admin/default-config" view=DefaultConfig/> + <Route ssr=SsrMode::PartiallyBlocked path="/admin/:tenant/default-config" view=DefaultConfig/> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/overrides" + view=ContextOverride + /> <Route path="/*any" view=NotFound/> + </Routes> </Layout> </body> diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index ca9e32e1c..b58b8748c 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -22,9 +22,9 @@ pub fn ContextForm( }); view! { - <div class="form-control w-full"> + <div class="form-control w-full "> <label class="label"> - <span class="label-text">Context</span> + <span class="label-text font-semibold text-lg">Context</span> </label> <div class="p-4"> <For @@ -35,7 +35,6 @@ pub fn ContextForm( .enumerate() .collect::<Vec<(usize, (String, String, String))>>() } - key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) children=move |(idx, (dimension, operator, value))| { let dimension_label = dimension.to_string(); @@ -46,12 +45,13 @@ pub fn ContextForm( <label class="label font-medium font-mono text-sm"> <span class="label-text">Operator</span> </label> - <select class="select select-bordered"> + <select class="select select-bordered w-full bg-black text-white text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline"> <option disabled selected> Pick one </option> - <option value="==">"=="</option> - <option value="!=">"!="</option> + <option value="==">==</option> + <option value="!=">!=</option> + <option value="!=">IN</option> </select> </div> @@ -63,10 +63,10 @@ pub fn ContextForm( <input type="text" placeholder="Type here" - class="input input-bordered w-full max-w-xs" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" /> <button - class="text-error text-xl font-light font-thin" + class="btn btn-error btn-circle" on:click=move |_| { set_context .update(|value| { diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 65ab87001..82c73ccd5 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -116,8 +116,6 @@ pub fn ExperimentForm( </div> <div> <OverrideForm - iv_rs=iv_rs - iv_ws=iv_ws overrides=variant_rs.get().overrides default_config=config /> diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index bfb714eda..8931c9bde 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,74 +1,78 @@ -use crate::{pages::ExperimentList::types::DefaultConfig, types::InputVector}; +use crate::pages::ExperimentList::types::DefaultConfig; use leptos::*; use serde_json::{Map, Value}; +use std::collections::HashSet; #[component] pub fn OverrideForm( overrides: Map<String, Value>, default_config: Vec<DefaultConfig>, - iv_rs: ReadSignal<InputVector>, - iv_ws: WriteSignal<InputVector>, ) -> impl IntoView { let (overrides, set_overrides) = create_signal(overrides); view! { - <For - each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } - key=|(config_key, _)| config_key.to_string() - children=move |(config_key, config_value)| { - let configs = default_config.clone(); - let config_key_copy = config_value.clone(); - view! { - <div> - <div class="flex gap-x-6"> - <div class="form-control w-20"> - <select class="select select-bordered"> - <option disabled selected> - Select Config - </option> - <For - each=move || configs.clone() - key=|item: &DefaultConfig| item.key.to_string() - children=move |item: DefaultConfig| { - view! { - <option - value=item.key.to_string() - selected=item.key.to_string() == config_key_copy - > - {item.key} - </option> + <div class="space-y-4 "> + <label class="label"> + <span class="label-text font-semibold text-lg">Override</span> + </label> + <For + each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } + key=|(config_key, _)| config_key.to_string() + children=move |(config_key, config_value)| { + let configs = default_config.clone(); + let config_key_copy = config_value.clone(); + view! { + <div class="card bg-base-100 shadow-xl"> + <div class="card-body"> + <div class="flex items-center gap-4"> + <select class="select select-bordered w-full bg-gray-200 text-white-400/50"> + <option disabled selected class="text-sky-400/50 font-mono"> + Select Config + </option> + <For + each=move || configs.clone() + key=|item: &DefaultConfig| item.key.to_string() + children=move |item: DefaultConfig| { + view! { + <option + value=item.key.to_string() + selected=item.key.to_string() == config_key_copy + > + {item.key} + </option> + } } - } - /> + /> - </select> - </div> - <div class="form-control"> - <div class="flex gap-x-6 items-center"> - <input - type="text" - placeholder="Type here" - name="override[]" - value=config_value.to_string() - class="input input-bordered w-full max-w-xs" - /> - <button - class="text-error text-xl font-light font-thin" - on:click=move |_| { - set_overrides - .update(|value| { - value.remove(&config_key); - }); - } - > + </select> + </div> + <div class="form-control flex-grow"> + <div class="flex gap-x-6 items-center"> + <input + type="text" + placeholder="Type here" + value=config_value.to_string() + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + /> + <button + class="btn btn-error btn-circle" + on:click=move |_| { + set_overrides + .update(|value| { + value.remove(&config_key); + }); + } + > - <i class="ri-delete-bin-2-line"></i> - </button> + <i class="ri-delete-bin-2-line"></i> + </button> + </div> </div> </div> </div> - </div> + } } - } - /> + /> + + </div> } } diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index d3fd5caf3..a7e33cb0e 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -39,7 +39,9 @@ pub fn Table( {columns .iter() .filter(|column| !column.hidden) - .map(|column| view! { <th class="uppercase">{&column.name}</th> }) + .map(|column| { + view! { <th class="uppercase font-mono">{&column.name}</th> } + }) .collect_view()} </tr> @@ -89,7 +91,10 @@ pub fn Table( let value: String = generate_table_row_str( row.get(cname).unwrap_or(&Value::String("".to_string())), ); - view! { <td>{(column.formatter)(&value, &row)}</td> } + view! { + <td class=table_style + .to_string()>{(column.formatter)(&value, row)}</td> + } }) .collect_view()} diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index d5dc4478d..bf13157e9 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -3,9 +3,10 @@ use serde_json::{Map, Value}; pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; -#[derive(Clone, Debug)] -pub struct TableSettings { - pub redirect_prefix: Option<String>, +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub key: String, + pub value: String, } #[derive(Clone, PartialEq)] @@ -15,7 +16,7 @@ pub struct Column { pub formatter: CellFormatter, } -fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { +fn default_formatter(value: &str, row: &Map<String, Value>) -> View { view! { <span>{value.to_string()}</span> }.into_view() } diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs new file mode 100644 index 000000000..0ce4207d9 --- /dev/null +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -0,0 +1,455 @@ +// use std::collections::HashMap; +use std::rc::Rc; + +use crate::components::context_form::context_form::ContextForm; +use crate::components::override_form::override_form::OverrideForm; +use crate::components::table::{table::Table, types::Column}; +use crate::pages::DefaultConfig::types::Config; +use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; +use leptos::ev::SubmitEvent; +// use leptos::spawn_local; +use leptos::svg::view; +use leptos::*; +use leptos_router::use_query_map; +use serde_json::{Map, Value}; + +pub async fn fetch_config(tenant: String) -> Result<Config, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +// pub async fn create_context( +// tenant: String, +// key: String, +// value: String, +// key_type: String, +// pattern: String, +// ) -> Result<String, String> { +// let client = reqwest::Client::new(); +// let host = "http://localhost:8080"; +// let url = format!("{host}/context"); +// let mut req_body: HashMap<&str, Value> = HashMap::new(); +// let mut schema: Map<String, Value> = Map::new(); +// schema.insert("type".to_string(), Value::String(key_type)); +// schema.insert("pattern".to_string(), Value::String(pattern)); +// req_body.insert("value", Value::String(value)); +// req_body.insert("schema", Value::Object(schema)); +// let response = client +// .put(url) +// .header("x-tenant", tenant) +// .header("Authorization", "Bearer 12345678") +// .json(&req_body) +// .send() +// .await +// .map_err(|e| e.to_string())?; +// response.text().await.map_err(|e| e.to_string()) +// } + +pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/dimension"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let dimensions = response.json().await.map_err(|e| e.to_string())?; + Ok(dimensions) + } + Err(e) => Err(e.to_string()), + } +} + +#[component] +fn ContextModalForm() -> impl IntoView { + let query = use_query_map(); + + let tenant = query.with(|params_map| { + params_map + .get("tenant") + .cloned() + .unwrap_or_else(|| "mjos".to_string()) + }); + let tenant = tenant.clone(); + + let dimensions = + create_blocking_resource(|| {}, move |_| fetch_dimensions(tenant.clone())); + + let context = use_context::<RwSignal<Vec<(String, String, String)>>>(); + + view! { + <div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {move || { + dimensions + .with(move |result| { + match result { + Some(Ok(dimension)) => { + view! { + <div> + <ContextForm + dimensions=dimension.clone() + context=context.unwrap().get().clone() + /> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + } + } + None => { + view! { <div>Loading....</div> } + } + } + }) + }} + + </Suspense> + </div> + } +} + +pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/default-config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let default_config = response.json().await.map_err(|e| e.to_string())?; + Ok(default_config) + } + Err(e) => Err(e.to_string()), + } +} + +#[component] +fn OverrideModalForm() -> impl IntoView { + let query = use_query_map(); + + let tenant = query.with(|params_map| { + params_map + .get("tenant") + .cloned() + .unwrap_or_else(|| "mjos".to_string()) + }); + let tenant = tenant.clone(); + + let default_config = + create_blocking_resource(|| {}, move |_| fetch_default_config(tenant.clone())); + + let override_data = use_context::<RwSignal<Map<String, Value>>>(); + + view! { + <div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {move || { + default_config + .with(move |result| { + match result { + Some(Ok(config)) => { + view! { + <div> + <OverrideForm + overrides=override_data.unwrap().get().clone() + default_config=config.clone() + /> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + } + } + None => { + view! { <div>Loading....</div> } + } + } + }) + }} + + </Suspense> + </div> + } +} + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} + +// Vec[Condition] -> Update it. +// Vec[Override] -> Update it. + +#[component] +fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { + view! { + <div class="p-6 text-gray-600 space-y-6"> + <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> + Create Context Overrides + <i class="ri-edit-2-line ml-2"></i> + </button> + // + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + // on:submit=on_submit + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> + <ContextModalForm/> + <OverrideModalForm/> + <div class="form-control mt-6"> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit + </button> + </div> + </form> + </div> + </dialog> + </div> + } +} + +fn parse_conditions(input: String) -> Vec<(String, String, String)> { + let mut conditions = Vec::new(); + let operators = vec!["==", "in", "!="]; // Define your operators here + + // Split the string by "and" and iterate over each condition + for condition in input.split("and") { + let mut parts = Vec::new(); + let mut operator_found = ""; + + // Check for each operator + for operator in &operators { + if condition.contains(operator) { + operator_found = operator; + parts = condition.split(operator).collect(); + break; + } + } + + if parts.len() == 2 { + conditions.push(( + parts[0].trim().to_string(), + operator_found.to_string(), + parts[1].trim().to_string(), + )); + } + } + + conditions +} + +#[component] +pub fn ContextOverride() -> impl IntoView { + let query = use_query_map(); + + let tenant = query.with(|params_map| { + params_map + .get("tenant") + .cloned() + .unwrap_or_else(|| "mjos".to_string()) + }); + + let context_data: Vec<(String, String, String)> = vec![]; + let ctx: RwSignal<Vec<(String, String, String)>> = create_rw_signal(context_data); + + let override_data = Map::new(); + + let ovr_data = create_rw_signal(override_data); + + provide_context(ctx); + + provide_context(ovr_data); + + let config_data = + create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); + + let table_columns = create_memo(move |_| { + vec![ + Column::default("KEY".to_string()), + Column::default("VALUE".to_string()), + ] + }); + + view! { + <div class="p-8 space-y-6 bg-gray-120"> + <div class="container mx-auto space-y-6 p-8"> + <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {move || { + config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let mut contexts: Vec<Map<String, Value>> = Vec::new(); + let mut context_views = Vec::new(); + let mut new_ctx: Vec<(String, String, String)> = vec![]; + let mut override_signal = Map::new(); + for context in config.contexts.iter() { + let condition = extract_and_format(&context.condition); + let ctx_values = parse_conditions(condition.clone()); + new_ctx.extend(ctx_values); + for key in context.override_with_keys.iter() { + let mut map = Map::new(); + let ovr = config.overrides.get(key).unwrap(); + let ovr_obj = ovr.as_object().unwrap(); + for (key, value) in ovr_obj.iter() { + let trimmed_key = Value::String( + key.trim_matches('"').to_string(), + ); + let formatted_value = Value::String( + format!("{}", value).trim_matches('"').to_string(), + ); + override_signal + .insert(trimmed_key.to_string(), formatted_value.clone()); + map.insert("KEY".to_string(), trimmed_key); + map.insert("VALUE".to_string(), formatted_value); + contexts.push(map.clone()); + } + } + context_views + .push( + view! { + <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6"> + <div class="card-body bg-white dark:bg-gray-800 bg-white shadow-md"> + <div class="flex justify-between"> + <div class="flex items-center space-x-4"> + <i class="ri-settings-5-line ri-xl text-blue-500"></i> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> + "Condition" + </h2> + <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> + <div class="badge badge-primary font-mono">{condition}</div> + </div> + <div class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> + <i class="ri-edit-line text-blue-500"></i> + </div> + </div> + <div class="space-x-4"> + <Table + table_style="font-mono".to_string() + rows=contexts.clone() + key_column="id".to_string() + columns=table_columns.get() + /> + </div> + </div> + + </div> + }, + ); + contexts.clear(); + } + ctx.set(new_ctx); + ovr_data.set(override_signal); + context_views + } + Some(Err(error)) => { + vec![ + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + }, + ] + } + None => vec![view! { <div>Loading....</div> }], + } + }) + }} + + </Suspense> + </div> + </div> + } +} diff --git a/crates/frontend/src/pages/ContextOverride/mod.rs b/crates/frontend/src/pages/ContextOverride/mod.rs new file mode 100644 index 000000000..0f1d89ff1 --- /dev/null +++ b/crates/frontend/src/pages/ContextOverride/mod.rs @@ -0,0 +1 @@ +pub mod ContextOverride; diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 7282d1020..104eccaaa 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -1,8 +1,12 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::components::table::{table::Table, types::Column}; +use crate::components::table::{ + table::Table, + types::{Column, RowData}, +}; use crate::pages::DefaultConfig::types::Config; +use js_sys; use leptos::ev::SubmitEvent; use leptos::spawn_local; use leptos::*; @@ -11,13 +15,7 @@ use serde_json::{Map, Value}; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = "http://localhost:8080"; let url = format!("{host}/config"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { @@ -36,13 +34,7 @@ pub async fn create_default_config( pattern: String, ) -> Result<String, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = "http://localhost:8080"; let url = format!("{host}/default-config/{key}"); let mut req_body: HashMap<&str, Value> = HashMap::new(); let mut schema: Map<String, Value> = Map::new(); @@ -71,7 +63,12 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { </button> <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> <div class="modal-box relative bg-white"> - <FormComponent handle_submit = handle_submit/> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + <FormComponent handle_submit=handle_submit/> </div> </dialog> </div> @@ -82,12 +79,21 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { use leptos::html::Input; let handle_submit = handle_submit.clone(); + let global_state = use_context::<RwSignal<RowData>>(); + let row_data = global_state.unwrap().get(); - let (key, set_key) = create_signal("key1".to_string()); - let (value, set_value) = create_signal("value1".to_string()); + let (key, set_key) = create_signal(row_data.key); + let (value, set_value) = create_signal(row_data.value); let (keytype, set_keytype) = create_signal("string".to_string()); let (pattern, set_pattern) = create_signal(".*".to_string()); + create_effect(move |_| { + if let Some(row_data) = global_state { + set_key.set(row_data.get().key.clone().to_string()); + set_value.set(row_data.get().value.clone().to_string()); + } + }); + let input_element: NodeRef<Input> = create_node_ref(); let input_element_two: NodeRef<Input> = create_node_ref(); let input_element_three: NodeRef<Input> = create_node_ref(); @@ -136,12 +142,18 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { }; view! { - <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit> + <form + class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" + on:submit=on_submit + > <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Key</span> </label> - <input type="text" placeholder="Key" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + <input + type="text" + placeholder="Key" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=key node_ref=input_element /> @@ -150,7 +162,10 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Value</span> </label> - <input type="text" placeholder="Value" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + <input + type="text" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=value node_ref=input_element_two /> @@ -159,7 +174,10 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Type</span> </label> - <input type="text" placeholder="Type" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + <input + type="text" + placeholder="Type" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=keytype node_ref=input_element_three /> @@ -168,20 +186,64 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> </label> - <input type="text" placeholder="Pattern" class="input input-bordered w-full bg-white text-gray-700 shadow-md" + <input + type="text" + placeholder="Pattern" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=pattern node_ref=input_element_four /> </div> <div class="form-control mt-6"> - <button type="submit" class="btn btn-primary shadow-md font-mono" onclick="my_modal_5.close()">Submit</button> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit + </button> </div> </form> } } +fn custom_formatter(value: &str, row: &Map<String, Value>) -> View { + let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); + let row_key = row["KEY"].clone().to_string().replace("\"", ""); + let row_value = row["VALUE"].clone().to_string().replace("\"", ""); + + let edit_click_handler = move |_| { + let row_data = RowData { + key: row_key.clone(), + value: row_value.clone(), + }; + intermediate_signal.set(Some(row_data)); + js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); + }; + + let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + + view! { <span>{edit_icon}</span> } + .into_view() +} + #[component] pub fn DefaultConfig() -> impl IntoView { + // let (edit_row_data, set_edit_row_data) = create_signal(None); + let global_state = create_rw_signal(RowData::default()); + provide_context(global_state); + + let intermediate_signal = create_rw_signal(None::<RowData>); + + // Listener for intermediate signal + create_effect(move |_| { + if let Some(row_data) = intermediate_signal.get() { + global_state.set(row_data.clone()); + } + }); + + provide_context(intermediate_signal.clone()); + let query = use_query_map(); let tenant = query.with(|params_map| { @@ -198,64 +260,69 @@ pub fn DefaultConfig() -> impl IntoView { vec![ Column::default("KEY".to_string()), Column::default("VALUE".to_string()), + Column::new("EDIT".to_string(), None, Some(custom_formatter)), ] }); view! { <div class="p-8"> - <ModalComponent handle_submit = Rc::new(move || config_data.refetch()) /> - <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> - { - move || config_data.with( move |result| { - match result { - Some(Ok(config)) => { - let mut default_config: Vec<Map<String, Value>> = Vec::new(); + <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> - for (key, value) in config.default_configs.iter() { - let mut map = Map::new(); + {move || { + config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let mut default_config: Vec<Map<String, Value>> = Vec::new(); + for (key, value) in config.default_configs.iter() { + let mut map = Map::new(); + let trimmed_key = Value::String( + key.trim_matches('"').to_string(), + ); + let formatted_value = Value::String( + format!("{}", value).trim_matches('"').to_string(), + ); + map.insert("KEY".to_string(), trimmed_key); + map.insert("VALUE".to_string(), formatted_value); + default_config.push(map); + } + vec![ + view! { + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> + "Default Config" + </h2> + <Table + table_style="font-mono".to_string() + rows=default_config + key_column="id".to_string() + columns=table_columns.get() + /> + </div> - let trimmed_key = Value::String(key.trim_matches('"').to_string()); - let formatted_value = Value::String(format!("{}", value).trim_matches('"').to_string()); - - map.insert("KEY".to_string(), trimmed_key); - map.insert("VALUE".to_string(), formatted_value); - default_config.push(map); - } - - vec![ - view! { - <div class="card rounded-lg w-full bg-base-100 shadow"> - <div class="card-body"> - <h2 class="card-title">Default Config</h2> - <Table - table_style="hover".to_string() - rows={default_config} - key_column="id".to_string() - columns={table_columns.get()} - /> - </div> - - </div> + </div> + }, + ] + } + Some(Err(error)) => { + vec![ + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + }, + ] + } + None => vec![view! { <div>Loading....</div> }], } - ] - }, - Some(Err(error)) => { - vec![ - view! { - <div class="text-red-500"> - {"Failed to fetch config data: "} - {error} - </div> - } - ] - }, - None => { vec![view! {<div>Loading....</div> }]}, - } - }) - } - </Suspense> - </div> - + }) + }} + </Suspense> + </div> } } diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index c12097e77..649f99b1a 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -69,11 +69,10 @@ pub fn Dimensions() -> impl IntoView { println!("hello1: {:?}", data); view! { <Table - _table_style="abc".to_string() + table_style="abc".to_string() rows=data - _key_column="id".to_string() + key_column="id".to_string() columns=table_columns.get() - settings={TableSettings {redirect_prefix: None}} /> } } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index d37e1de26..ab886a406 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -6,10 +6,7 @@ use chrono::{prelude::Utc, TimeZone}; use crate::components::{ experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, - table::{ - table::Table, - types::{Column, TableSettings}, - }, + table::{table::Table, types::Column}, }; use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; @@ -114,9 +111,6 @@ pub fn ExperimentList() -> impl IntoView { }} </div> - <div class="stat cursor-pointer" onclick="create_exp_modal.showModal()"> - <div class="stat-figure text-primary">new</div> - </div> </div> </div> <div class="card rounded-xl w-full bg-base-100 shadow"> @@ -125,12 +119,6 @@ pub fn ExperimentList() -> impl IntoView { <div> {move || { - let current_tenant = tenant_rs.get(); - let settings = TableSettings { - redirect_prefix: Some( - format!("admin/{current_tenant}/experiments"), - ), - }; let value = experiments.get(); match value { Some(v) => { @@ -142,11 +130,10 @@ pub fn ExperimentList() -> impl IntoView { .to_owned(); view! { <Table - _table_style="abc".to_string() + table_style="abc".to_string() rows=data - _key_column="id".to_string() + key_column="id".to_string() columns=table_columns.get() - settings=settings /> } } @@ -202,20 +189,13 @@ pub fn ExperimentList() -> impl IntoView { let dim = dimensions.get().unwrap_or(vec![]); let def_conf = default_config.get().unwrap_or(vec![]); view! { - <dialog id="create_exp_modal" class="modal"> - <div class="modal-box"> - <h3 class="font-bold text-lg">Create an Experiment</h3> - <div class="modal-action flex flex-col"> - <ExperimentForm - name="".to_string() - context=vec![] - variants=vec![] - dimensions=dim - default_config=def_conf - /> - </div> - </div> - </dialog> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim + default_config=def_conf + /> } }} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 996a2eb0f..7b0d5ae80 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,3 +1,4 @@ +pub mod ContextOverride; pub mod DefaultConfig; pub mod Dimensions; pub mod Experiment; diff --git a/flake.nix b/flake.nix index d83cb7597..f3a6ff618 100644 --- a/flake.nix +++ b/flake.nix @@ -52,6 +52,7 @@ awscli jq nodejs_18 + leptosfmt wasm-pack leptosfmt ( rust-bin.stable.latest.default.override { From 3ce29f05bd0103fd825639a467dde0f611f77479 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Mon, 20 Nov 2023 17:39:33 +0530 Subject: [PATCH 204/352] feat: experiment UI --- crates/frontend/src/app.rs | 2 + .../src/components/nav_item/nav_item.rs | 6 +- .../src/components/side_nav/side_nav.rs | 2 +- crates/frontend/src/components/table/table.rs | 58 +- crates/frontend/src/components/table/types.rs | 5 +- crates/frontend/src/pages/Experiment/mod.rs | 611 +++--------------- .../pages/ExperimentList/ExperimentList.rs | 40 +- crates/frontend/src/pages/Home/Home.rs | 202 +++--- .../frontend/src/pages/NotFound/NotFound.rs | 5 +- crates/frontend/src/pages/mod.rs | 1 + 10 files changed, 268 insertions(+), 664 deletions(-) diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index ba319694f..c53bbec5e 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -18,6 +18,8 @@ pub fn App() -> impl IntoView { provide_context(tenant_rs); provide_context(tenant_ws); view! { + // injects a stylesheet into the document <head> + // id=leptos means cargo-leptos will hot-reload this stylesheet <Stylesheet id="leptos" href="/pkg/style.css"/> // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 4ca872413..142182108 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -23,9 +23,9 @@ pub fn NavItem( }; view! { - <A href=href class=anchor_class> - <div class=icon_wrapper_class> - <i class=icon_class></i> + <A href={href} class={anchor_class}> + <div class={icon_wrapper_class}> + <i class={icon_class} /> </div> <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 4304bdcee..88c282160 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -70,7 +70,7 @@ pub fn SideNav() -> impl IntoView { <For each=move || app_routes.get() key=|route: &AppRoute| route.key.to_string() - children=move |route: AppRoute| { + view=move | route: AppRoute| { let path = route.path.to_string(); let is_active = location.pathname.get().contains(&path); view! { diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index a7e33cb0e..656045c11 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -35,15 +35,15 @@ pub fn Table( <thead> <tr> <th></th> - - {columns - .iter() - .filter(|column| !column.hidden) - .map(|column| { - view! { <th class="uppercase font-mono">{&column.name}</th> } - }) - .collect_view()} - + { + columns + .iter() + .filter(|column| !column.hidden) + .map(|column| view! { + <th class="uppercase">{&column.name}</th> + }) + .collect_view() + } </tr> </thead> <tbody> @@ -82,27 +82,27 @@ pub fn Table( > <th>{index + 1}</th> - - {columns - .iter() - .filter(|column| !column.hidden) - .map(|column| { - let cname = &column.name; - let value: String = generate_table_row_str( - row.get(cname).unwrap_or(&Value::String("".to_string())), - ); - view! { - <td class=table_style - .to_string()>{(column.formatter)(&value, row)}</td> - } - }) - .collect_view()} - + { + columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + let cname = &column.name; + let value: String = generate_table_row_str( + row + .get(cname) + .unwrap_or(&Value::String("".to_string())) + ); + view! { + <td>{(column.formatter)(&value, row)}</td> + } + }) + .collect_view() + } </tr> - } - }) - .collect_view()} - + }) + .collect_view() + } </tbody> </table> </div> diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index bf13157e9..94a06c370 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -17,7 +17,10 @@ pub struct Column { } fn default_formatter(value: &str, row: &Map<String, Value>) -> View { - view! { <span>{value.to_string()}</span> }.into_view() + view! { + <span>{value.to_string()}</span> + } + .into_view() } impl Column { diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 828e0bb96..71bba152a 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,533 +1,118 @@ -use chrono::{DateTime, Utc}; -use leptos::{html::Input, logging::log, *}; +use leptos::*; use leptos_router::use_params_map; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Map, Value}; -use tracing::debug; -use web_sys::SubmitEvent; - -use crate::components::{ - experiment_form::experiment_form::ExperimentForm, - table::{ - table::Table, - types::{Column, TableSettings}, - }, -}; - -use super::ExperimentList::utils::{fetch_default_config, fetch_dimensions}; - -#[derive( - Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, -)] -#[strum(serialize_all = "UPPERCASE")] -pub(crate) enum ExperimentStatusType { - CREATED, - INPROGRESS, - CONCLUDED, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, strum_macros::Display)] -#[strum(serialize_all = "UPPERCASE")] -pub(crate) enum VariantType { - CONTROL, - EXPERIMENTAL, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct Variant { - pub id: String, - pub override_id: String, - pub context_id: String, - pub overrides: Value, - pub(crate) variant_type: VariantType, -} - -pub type Variants = Vec<Variant>; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Experiment { - pub(crate) variants: Variants, - pub(crate) name: String, - pub(crate) id: String, - pub(crate) traffic_percentage: u8, - pub(crate) context: Value, - pub(crate) status: ExperimentStatusType, - pub(crate) override_keys: Value, - pub(crate) created_by: String, - pub(crate) created_at: DateTime<Utc>, - pub(crate) last_modified: DateTime<Utc>, - pub(crate) chosen_variant: Option<String>, -} - -async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - match client - .get(format!("http://localhost:8080/experiments/{}", exp_id)) - .header("x-tenant", tenant) - .send() - .await - { - Ok(experiment) => { - debug!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } -} - -async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - match client - .patch(format!("http://localhost:8080/experiments/{}/ramp", exp_id)) - .header("x-tenant", "mjos") - .json(&json!({ "traffic_percentage": percent })) - .send() - .await - { - Ok(experiment) => { - debug!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } -} - -async fn conclude_experiment( - exp_id: String, - variant_id: String, -) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - match client - .patch(format!( - "http://localhost:8080/experiments/{}/conclude", - exp_id - )) - .header("x-tenant", "mjos") - .json(&json!({ "chosen_variant": variant_id })) - .send() - .await - { - Ok(experiment) => { - debug!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } -} #[component] pub fn experiment_page() -> impl IntoView { let exp_params = use_params_map(); - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let source = move || { - let t = tenant_rs.get(); - let exp_id = - exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); - (exp_id, t) - }; - - let experiment_info = create_resource(source, |(exp_id, tenant)| async move { - get_experiment(&exp_id, &tenant).await - }); - view! { - <Transition fallback=move || { - view! { <h1>Loading....</h1> } - }> - {move || match experiment_info.get() { - Some(Ok(experiment)) => { - experiment_detail_view(experiment, experiment_info).into_view() - } - Some(Err(err)) => view! { <h1>{err.to_string()}</h1> }.into_view(), - None => view! { <h1>No elements</h1> }.into_view(), - }} - - </Transition> - } -} - -fn experiment_detail_view( - initial_data: Experiment, - exp_resource: Resource<(String, String), Result<Experiment, String>>, -) -> impl IntoView { - let contexts = initial_data.context["and"] - .as_array() - .unwrap_or(&Vec::new()) - .to_owned(); - let (experiment, _) = create_signal(initial_data); - let (ctxs, _) = create_signal(contexts); + let experiment_id = + move || exp_params.with(|params| params.get("id").cloned().unwrap()); view! { <div class="flex flex-col overflow-x-auto p-2"> - {move || { - experiment - .with(|exp| { - let class_name = match exp.status { - ExperimentStatusType::CREATED => { - "badge ml-3 mb-1 badge-lg badge-primary" - } - ExperimentStatusType::INPROGRESS => { - "badge ml-3 mb-1 badge-lg badge-warning" - } - ExperimentStatusType::CONCLUDED => { - "badge ml-3 mb-1 badge-lg badge-success" - } - }; - view! { - <h1 class="text-4xl pt-4 font-extrabold"> - {&exp.name} <span class=class_name>{exp.status.to_string()}</span> - </h1> - } - }) - }} - <div class="divider"></div> - <div class="flex flex-row justify-end join m-5"> - {move || { - experiment - .with(|exp| { - match exp.status { - ExperimentStatusType::CREATED => { - view! { - <button - class="btn join-item" - onclick="edit_exp_modal.showModal()" - > - <i class="ri-edit-line"></i> - Edit - </button> - <button - class="btn join-item" - value=&exp.id - on:click=move |button_event| spawn_local(async move { - let value = event_target_value(&button_event); - let _ = ramp_experiment(&value, 1).await; - exp_resource.refetch(); - }) - > - - <i class="ri-guide-line"></i> - Start - </button> - } - } - ExperimentStatusType::INPROGRESS => { - view! { - // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> - <button - class="btn join-item" - onclick="conclude_exp_modal.showModal()" - > - <i class="ri-stop-circle-line"></i> - Conclude - </button> - <button - class="btn join-item" - onclick="ramp_exp_modal.showModal()" - > - <i class="ri-flight-takeoff-line"></i> - Ramp - </button> - } - } - ExperimentStatusType::CONCLUDED => { - view! { - // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> - - // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> - - // <button class="btn join-item" onclick="conclude_exp_modal.showModal()"><i class="ri-stop-circle-line"></i>Conclude</button> - <div></div> - <div class="stat"> - <div class="stat-title">Chosen Variant</div> - <div class="stat-value"> - {match exp.chosen_variant { - Some(ref v) => format!("{}", v), - None => String::new(), - }} + <h1 class="text-4xl pt-4 font-extrabold"> + Experiment Name + <span class="badge ml-3 mb-1 badge-primary badge-lg">Created</span> + </h1> + + <div class="divider"></div> + + <div class="join m-5"> + <button class="btn join-item"><i class="ri-edit-line"></i>Edit</button> + <button class="btn join-item"><i class="ri-stop-circle-line"></i>Conclude</button> + <button class="btn join-item"><i class="ri-guide-line"></i>Release</button> + <button class="btn join-item"><i class="ri-flight-takeoff-line"></i>Ramp</button> + </div> - </div> - </div> - } - } - } - }) - }} + <div class="stats bg-primary shadow-xl mt-5 text-primary-content"> + <div class="stat"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value">{experiment_id}</div> + </div> + <div class="stat"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value">0</div> + </div> + <div class="stat"> + <div class="stat-title">Created at</div> + <div class="stat-value">19/11/2023</div> + </div> + <div class="stat"> + <div class="stat-title">Created by</div> + <div class="stat-value">mobius@juspay.in</div> + </div> + </div> - </div> <div class="stats shadow-xl mt-5"> - <div class="stat"> - <div class="stat-title">Experiment ID</div> - <div class="stat-value">{experiment.get().id}</div> - </div> - <div class="stat"> - <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value">{move || experiment.get().traffic_percentage}</div> - </div> - <div class="stat"> - <div class="stat-title">Created by</div> - <div class="stat-value">{experiment.get().created_by}</div> - </div> - <div class="stat"> - <div class="stat-title">Created at</div> - <div class="stat-value"> - {format!("{}", experiment.get().created_at.format("%v"))} + <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row"> + <div class="stat"> + <div class="stat-title">Client ID</div> + <div class="stat-value">cac</div> </div> - </div> - <div class="stat"> - <div class="stat-title">Last Modified</div> - <div class="stat-value"> - {move || { - experiment.with(|exp| format!("{}", &exp.last_modified.format("%v"))) - }} - + <div class="divider divider-horizontal">&&</div> + <div class="stat"> + <div class="stat-title">OS</div> + <div class="stat-value">android</div> </div> </div> - </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Context</h2> - <div class="flex flex-row"> - {move || { - let contexts = move || ctxs.get().into_iter(); - let mut view: Vec<_> = Vec::new(); - for item in contexts() { - for (_, value) in item.as_object().unwrap().into_iter() { - let rule_vector = value.as_array().unwrap(); - let mut rule_iter = rule_vector.to_owned().into_iter(); - let (var, value) = ( - rule_iter.next().unwrap(), - rule_iter.next().unwrap(), - ); - let dimension = var.as_object().unwrap().get("var").unwrap(); - view.push( - view! { - <div class="stat"> - <div class="stat-title"> - {format!("{}", dimension.as_str().unwrap())} - </div> - <div class="stat-value"> - {format!("{}", value.as_str().unwrap())} - </div> - </div> - }, - ) - } - } - view - }} + </div> + </div> - </div> - </div> - </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Variants</h2> - <div class="overflow-x-auto overflow-y-auto"> - {move || { - let exp = move || experiment.get(); - let rows = gen_variant_rows(&exp().variants).unwrap(); - let mut columns: Vec<Column> = Vec::new(); - let settings = TableSettings { - redirect_prefix: None, - }; - columns.push(Column::default("Variant".into())); - for okey in exp().override_keys.as_array().unwrap().into_iter() { - columns.push(Column::default(okey.as_str().unwrap().into())); - } - view! { - <Table - _table_style="abc".to_string() - rows=rows - _key_column="overrides".to_string() - columns=columns - settings=settings - /> - } - }} - </div> + <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto"> + <table class="table"> + <thead> + <tr> + <th></th> + <th>Key</th> + <th>Variant-1</th> + <th>Variant-2</th> + <th>Variant-3</th> + <th>Variant-4</th> + <th>Variant-5</th> + <th>Control</th> + </tr> + </thead> + <tbody> + <tr> + <th>1</th> + <td>pmTestKey1</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Quality Control Specialist</td> + <td>Blue</td> + <td>Blue</td> + </tr> + <tr> + <th>2</th> + <td>pmTestKey2</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Desktop Support Technician</td> + <td>Purple</td> + </tr> + <tr> + <th>3</th> + <td>pmTestKey3</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Tax Accountant</td> + <td>Red</td> + </tr> + </tbody> + </table> </div> </div> </div> - {add_dialogs(experiment, exp_resource)} - } -} - -fn gen_variant_rows(variants: &Vec<Variant>) -> Result<Vec<Map<String, Value>>, String> { - let mut rows: Vec<Map<String, Value>> = Vec::new(); - for (i, variant) in variants.into_iter().enumerate() { - let variant_name = match variant.variant_type { - VariantType::CONTROL => "Control".into(), - VariantType::EXPERIMENTAL => format!("Variant-{i}"), - }; - let mut m = Map::new(); - m.insert("Variant".into(), variant_name.into()); - m.insert("variant_id".into(), variant.id.clone().into()); - for (o, value) in variant.overrides.as_object().unwrap().into_iter() { - m.insert(o.clone(), value.clone()); - } - rows.push(m); - } - Ok(rows) -} - -fn add_dialogs( - experiment_rs: ReadSignal<Experiment>, - experiment_ws: Resource<(String, String), Result<Experiment, String>>, -) -> impl IntoView { - let input_element: NodeRef<Input> = create_node_ref(); - let experiment = move || experiment_rs.get(); - let (traffic, set_traffic) = create_signal(experiment().traffic_percentage); - - let on_submit = move |ev: SubmitEvent| { - ev.prevent_default(); - let value = input_element - .get() - .expect("<input> to exist") - .value_as_number() as u8; - spawn_local(async move { - let _ = ramp_experiment(&experiment().id, value).await; - experiment_ws.refetch(); - }); - }; - - let dimensions = create_resource( - || (), - |_| async move { - match fetch_dimensions().await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); - - let default_config = create_resource( - || (), - |_| async move { - match fetch_default_config().await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); - - match experiment_rs.get().status { - ExperimentStatusType::CREATED => view! { - <dialog id="edit_exp_modal" class="modal"> - <div class="modal-box"> - <h3 class="font-bold text-lg">Edit Experiment</h3> - <div class="modal-action"> - <ExperimentForm - name=experiment_rs.get().name - context=vec![] - variants=vec![] - dimensions=dimensions.get().unwrap_or(vec![]) - default_config=default_config.get().unwrap_or(vec![]) - /> - </div> - </div> - </dialog> - } - .into_view(), - ExperimentStatusType::INPROGRESS => view! { - <dialog id="conclude_exp_modal" class="modal"> - <div class="modal-box"> - <h3 class="font-bold text-lg">Conclude This Experiment</h3> - <p class="py-4"> - Choose a variant to conclude with, this variant becomes - the new default that is served to requests that match this context - </p> - <form method="dialog"> - {move || { - let mut view_arr = vec![]; - for (i, v) in experiment_rs.get().variants.into_iter().enumerate() { - let (variant, _) = create_signal(v); - let view = match variant.get().variant_type { - VariantType::CONTROL => { - view! { - <button - class="btn btn-block btn-outline btn-success m-2" - on:click=move |_| spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone()) - .await - .unwrap(); - experiment_ws.refetch(); - }) - > - - Control - </button> - } - } - VariantType::EXPERIMENTAL => { - view! { - <button - class="btn btn-block btn-outline btn-info m-2" - on:click=move |_| spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone()) - .await - .unwrap(); - experiment_ws.refetch(); - }) - > - - {format!("Variant-{i}")} - </button> - } - } - }; - view_arr.push(view); - } - view_arr - }} - - </form> - <div class="modal-action"> - <form method="dialog"> - <button class="btn">Close</button> - </form> - </div> - </div> - </dialog> - <dialog id="ramp_exp_modal" class="modal"> - <div class="modal-box"> - <h3 class="font-bold text-lg">Ramp up with release</h3> - <p class="py-4">Increase the traffic being redirected to the variants</p> - <form method="dialog" on:submit=on_submit> - <p>{move || traffic.get()}</p> - <input - type="range" - min="0" - max="100" - node_ref=input_element - value=move || experiment_rs.get().traffic_percentage - class="range" - on:input=move |et| { - let t = event_target_value(&et).parse::<u8>().unwrap(); - log!("traffic value:{t}"); - set_traffic.set(t); - } - /> - - <button class="btn btn-block btn-outline btn-success m-2">Set</button> - </form> - <div class="modal-action"> - <form method="dialog"> - <button class="btn">Close</button> - </form> - </div> - </div> - </dialog> - }.into_view(), - ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), + </div> } } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index ab886a406..d37e1de26 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -6,7 +6,10 @@ use chrono::{prelude::Utc, TimeZone}; use crate::components::{ experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, - table::{table::Table, types::Column}, + table::{ + table::Table, + types::{Column, TableSettings}, + }, }; use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; @@ -111,6 +114,9 @@ pub fn ExperimentList() -> impl IntoView { }} </div> + <div class="stat cursor-pointer" onclick="create_exp_modal.showModal()"> + <div class="stat-figure text-primary">new</div> + </div> </div> </div> <div class="card rounded-xl w-full bg-base-100 shadow"> @@ -119,6 +125,12 @@ pub fn ExperimentList() -> impl IntoView { <div> {move || { + let current_tenant = tenant_rs.get(); + let settings = TableSettings { + redirect_prefix: Some( + format!("admin/{current_tenant}/experiments"), + ), + }; let value = experiments.get(); match value { Some(v) => { @@ -130,10 +142,11 @@ pub fn ExperimentList() -> impl IntoView { .to_owned(); view! { <Table - table_style="abc".to_string() + _table_style="abc".to_string() rows=data - key_column="id".to_string() + _key_column="id".to_string() columns=table_columns.get() + settings=settings /> } } @@ -189,13 +202,20 @@ pub fn ExperimentList() -> impl IntoView { let dim = dimensions.get().unwrap_or(vec![]); let def_conf = default_config.get().unwrap_or(vec![]); view! { - <ExperimentForm - name="".to_string() - context=vec![] - variants=vec![] - dimensions=dim - default_config=def_conf - /> + <dialog id="create_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Create an Experiment</h3> + <div class="modal-action flex flex-col"> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim + default_config=def_conf + /> + </div> + </div> + </dialog> } }} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index dc58cd547..8527a501d 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -112,111 +112,101 @@ pub fn Home() -> impl IntoView { create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); view! { - <div class="container mt-5"> - <div class="text-center mb-4"> - <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> - </div> - <Suspense fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - }> - - {config_data - .with(move |result| { - match result { - Some(Ok(config)) => { - let rows = |k: &String, v: &Value| { - let key = k.replace("\"", "").trim().to_string(); - let value = format!("{}", v) - .replace("\"", "") - .trim() - .to_string(); - view! { - <tr> - <td class="fw-normal col w-50 shadow-sm"> - <div class="col">{key}</div> - </td> - <td class="fw-normal col w-50 shadow-sm"> - <div class="col">{value}</div> - </td> - </tr> - } - }; - let contexts_views: Vec<_> = config - .contexts - .iter() - .map(|context| { - let condition = extract_and_format(&context.condition); - let rows: Vec<_> = context - .override_with_keys - .iter() - .filter_map(|key| config.overrides.get(key)) - .flat_map(|ovr| ovr.as_object().unwrap().iter()) - .map(|(k, v)| { rows(&k, &v) }) - .collect(); - view! { - <h6 class="fw-normal font-monospace"> - "Condition: " - <span class="badge rounded-pill bg-secondary small"> - {&condition} - </span> - </h6> - <table class="table table-responsive table-bordered table-hover border-secondary"> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody class="bg-light">{rows}</tbody> - </table> - } - }) - .collect::<Vec<_>>(); - let new_context_views = contexts_views - .into_iter() - .rev() - .collect::<Vec<_>>(); - let default_config: Vec<_> = config - .default_configs - .iter() - .map(|(k, v)| { rows(&k, &v) }) - .collect(); - vec![ - view! { - <div class="mb-4 "> - {new_context_views} - <h6 class="mb-3 f-6 fw-normal font-monospace"> - "Default Configuration" - </h6> - <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody>{default_config}</tbody> - </table> - </div> - }, - ] - } - Some(Err(error)) => { - vec![ - view! { - <div class="error"> - {"Failed to fetch config data: "} {error} - </div> - }, - ] - } - None => { - vec![view! { <div class="error">{"No config data fetched"}</div> }] - } - } - })} - - </Suspense> - </div> + <div class="container mt-5" > + <div class="text-center mb-4"> + <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> + </div> + <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> + { + config_data.with(move |result| { + match result { + Some(Ok(config)) => { + let rows = |k:&String, v:&Value| { + let key = k.replace("\"", "").trim().to_string(); + let value = format!("{}", v).replace("\"", "").trim().to_string(); + view! { + <tr> + <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> + <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> + </tr> + } + }; + + let contexts_views: Vec<_> = config.contexts.iter().map(|context| { + let condition = extract_and_format(&context.condition); + let rows: Vec<_> = context.override_with_keys.iter() + .filter_map(|key| config.overrides.get(key)) + .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .map(|(k, v)| { + rows(&k,&v) + }).collect(); + + view! { + <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> + <table class="table table-responsive table-bordered table-hover border-secondary"> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody class="bg-light"> + { rows } + </tbody> + </table> + } + }).collect::<Vec<_>>(); + + let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); + let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ + rows(&k,&v) + }).collect(); + + vec![ + view! { + <div class="mb-4 "> + { new_context_views } + <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> + <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> + <thead class="table-primary border-secondary"> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody> + {default_config} + </tbody> + </table> + </div> + } + ] + }, + Some(Err(error)) => { + vec![ + view! { + <div class="error"> + {"Failed to fetch config data: "} + {error} + </div> + } + ] + }, + None => { + vec![ + view! { + <div class="error"> + {"No config data fetched"} + </div> + } + ] + } + } + }) + } + </Suspense> + </div> + + } } diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs index d50db7d11..d4e7591eb 100644 --- a/crates/frontend/src/pages/NotFound/NotFound.rs +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -15,5 +15,8 @@ pub fn NotFound() -> impl IntoView { let resp = expect_context::<leptos_actix::ResponseOptions>(); resp.set_status(actix_web::http::StatusCode::NOT_FOUND); } - view! { <h1>"Not Found"</h1> } + + view! { + <h1>"Not Found"</h1> + } } diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 7b0d5ae80..55d1035ab 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -5,3 +5,4 @@ pub mod Experiment; pub mod ExperimentList; pub mod Home; pub mod NotFound; +pub mod Experiment; From ba2eb38262b0f07f0428c309cbfbd62fc53b9c80 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Tue, 21 Nov 2023 20:08:38 +0530 Subject: [PATCH 205/352] feat: working experiments page --- .env.example | 2 +- .../src/components/side_nav/side_nav.rs | 2 +- crates/frontend/src/components/table/table.rs | 57 +++++---- crates/frontend/src/components/table/types.rs | 14 +-- .../pages/ContextOverride/ContextOverride.rs | 7 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 15 ++- .../src/pages/Dimensions/Dimensions.rs | 6 +- crates/frontend/src/pages/Experiment/mod.rs | 108 ++++++++++++++++-- .../pages/ExperimentList/ExperimentList.rs | 4 +- crates/frontend/src/pages/mod.rs | 1 - 10 files changed, 155 insertions(+), 61 deletions(-) diff --git a/.env.example b/.env.example index d399d4ffa..72de84f5b 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,6 @@ DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=false TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/,/default-config" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 88c282160..4304bdcee 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -70,7 +70,7 @@ pub fn SideNav() -> impl IntoView { <For each=move || app_routes.get() key=|route: &AppRoute| route.key.to_string() - view=move | route: AppRoute| { + children=move |route: AppRoute| { let path = route.path.to_string(); let is_active = location.pathname.get().contains(&path); view! { diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index 656045c11..1890ecef1 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -22,8 +22,8 @@ fn generate_table_row_str(row: &Value) -> String { #[component] pub fn Table( - _key_column: String, - _table_style: String, + key_column: String, + table_style: String, columns: Vec<Column>, rows: Vec<Map<String, Value>>, settings: TableSettings, @@ -35,15 +35,13 @@ pub fn Table( <thead> <tr> <th></th> - { - columns - .iter() - .filter(|column| !column.hidden) - .map(|column| view! { - <th class="uppercase">{&column.name}</th> - }) - .collect_view() - } + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| view! { <th class="uppercase">{&column.name}</th> }) + .collect_view()} + </tr> </thead> <tbody> @@ -82,27 +80,24 @@ pub fn Table( > <th>{index + 1}</th> - { - columns - .iter() - .filter(|column| !column.hidden) - .map(|column| { - let cname = &column.name; - let value: String = generate_table_row_str( - row - .get(cname) - .unwrap_or(&Value::String("".to_string())) - ); - view! { - <td>{(column.formatter)(&value, row)}</td> - } - }) - .collect_view() - } + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + let cname = &column.name; + let value: String = generate_table_row_str( + row.get(cname).unwrap_or(&Value::String("".to_string())), + ); + view! { <td class=table_style.to_string()>{(column.formatter)(&value, &row)}</td> } + }) + .collect_view()} + </tr> - }) - .collect_view() - } + } + }) + .collect_view()} + </tbody> </table> </div> diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index 94a06c370..d5dc4478d 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -3,10 +3,9 @@ use serde_json::{Map, Value}; pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; -#[derive(Clone, Debug, Default)] -pub struct RowData { - pub key: String, - pub value: String, +#[derive(Clone, Debug)] +pub struct TableSettings { + pub redirect_prefix: Option<String>, } #[derive(Clone, PartialEq)] @@ -16,11 +15,8 @@ pub struct Column { pub formatter: CellFormatter, } -fn default_formatter(value: &str, row: &Map<String, Value>) -> View { - view! { - <span>{value.to_string()}</span> - } - .into_view() +fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { + view! { <span>{value.to_string()}</span> }.into_view() } impl Column { diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 0ce4207d9..860d49f92 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; +use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; use crate::pages::DefaultConfig::types::Config; use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; @@ -268,7 +269,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { Create Context Overrides <i class="ri-edit-2-line ml-2"></i> </button> - // + // <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> <form method="dialog" class="flex justify-end"> @@ -372,6 +373,9 @@ pub fn ContextOverride() -> impl IntoView { match result { Some(Ok(config)) => { let mut contexts: Vec<Map<String, Value>> = Vec::new(); + let settings = TableSettings { + redirect_prefix: None + }; let mut context_views = Vec::new(); let mut new_ctx: Vec<(String, String, String)> = vec![]; let mut override_signal = Map::new(); @@ -421,6 +425,7 @@ pub fn ContextOverride() -> impl IntoView { rows=contexts.clone() key_column="id".to_string() columns=table_columns.get() + settings= settings.clone() /> </div> </div> diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 104eccaaa..90281cd02 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use crate::components::table::{ table::Table, - types::{Column, RowData}, + types::{Column, TableSettings}, }; use crate::pages::DefaultConfig::types::Config; use js_sys; @@ -13,6 +13,12 @@ use leptos::*; use leptos_router::use_query_map; use serde_json::{Map, Value}; +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub key: String, + pub value: String, +} + pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); let host = "http://localhost:8080"; @@ -223,8 +229,7 @@ fn custom_formatter(value: &str, row: &Map<String, Value>) -> View { let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; - view! { <span>{edit_icon}</span> } - .into_view() + view! { <span>{edit_icon}</span> }.into_view() } #[component] @@ -277,6 +282,9 @@ pub fn DefaultConfig() -> impl IntoView { match result { Some(Ok(config)) => { let mut default_config: Vec<Map<String, Value>> = Vec::new(); + let settings = TableSettings { + redirect_prefix: None + }; for (key, value) in config.default_configs.iter() { let mut map = Map::new(); let trimmed_key = Value::String( @@ -301,6 +309,7 @@ pub fn DefaultConfig() -> impl IntoView { rows=default_config key_column="id".to_string() columns=table_columns.get() + settings = settings /> </div> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 649f99b1a..e4f8aed05 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -59,6 +59,9 @@ pub fn Dimensions() -> impl IntoView { {move || { let value = dimensions.get(); + let settings = TableSettings { + redirect_prefix: None + }; match value { Some(v) => { let data = v @@ -70,9 +73,10 @@ pub fn Dimensions() -> impl IntoView { view! { <Table table_style="abc".to_string() - rows=data + rows= data key_column="id".to_string() columns=table_columns.get() + settings=settings /> } } diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 71bba152a..c4054b2cc 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,17 +1,99 @@ use leptos::*; use leptos_router::use_params_map; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::debug; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub(crate) enum ExperimentStatusType { + CREATED, + INPROGRESS, + CONCLUDED, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub(crate) enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub override_id: String, + pub context_id: String, + pub overrides: Value, + pub(crate) variant_type: VariantType, +} + +pub type Variants = Vec<Variant>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Experiment { + pub(crate) variants: Variants, + pub(crate) name: String, + pub(crate) id: String, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, + pub(crate) created_by: String, + pub(crate) created_at: DateTime<Utc>, + pub(crate) last_modified: DateTime<Utc>, + pub(crate) chosen_variant: Option<Variant>, +} + +async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .get(format!("http://localhost:8080/experiments/{}", exp_id)) + .header("x-tenant", "mjos") + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + }, + Err(e) => Err(e.to_string()), + } +} #[component] pub fn experiment_page() -> impl IntoView { let exp_params = use_params_map(); - let experiment_id = - move || exp_params.with(|params| params.get("id").cloned().unwrap()); + let experiment_id = move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + let experiment_info = create_resource( + experiment_id, + |exp_id: String| async move { + get_experiment(&exp_id).await + }, + ); + view! { + <Transition + fallback= move || view! {<h1> Loading.... </h1>} > + {move || + match experiment_info.get() { + Some(Ok(experiment)) => experiment_detail_view(&experiment).into_view(), + Some(Err(err)) => view! {<h1>{err.to_string()}</h1>}.into_view(), + None => view! {<h1>No elements </h1>}.into_view(), + } + } + </Transition> + } +} +fn experiment_detail_view(exp: &Experiment) -> impl IntoView { view! { <div class="flex flex-col overflow-x-auto p-2"> <h1 class="text-4xl pt-4 font-extrabold"> - Experiment Name - <span class="badge ml-3 mb-1 badge-primary badge-lg">Created</span> + {&exp.name} + <span class="badge ml-3 mb-1 badge-primary badge-lg">{exp.status.to_string()}</span> </h1> <div class="divider"></div> @@ -23,22 +105,26 @@ pub fn experiment_page() -> impl IntoView { <button class="btn join-item"><i class="ri-flight-takeoff-line"></i>Ramp</button> </div> - <div class="stats bg-primary shadow-xl mt-5 text-primary-content"> + <div class="stats shadow-xl mt-5"> <div class="stat"> <div class="stat-title">Experiment ID</div> - <div class="stat-value">{experiment_id}</div> + <div class="stat-value">{&exp.id}</div> </div> <div class="stat"> <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value">0</div> + <div class="stat-value">{exp.traffic_percentage}</div> + </div> + <div class="stat"> + <div class="stat-title">Created by</div> + <div class="stat-value">{&exp.created_by}</div> </div> <div class="stat"> <div class="stat-title">Created at</div> - <div class="stat-value">19/11/2023</div> + <div class="stat-value">{format!("{}", &exp.created_at.format("%d-%m-%Y %H:%M:%S"))}</div> </div> <div class="stat"> - <div class="stat-title">Created by</div> - <div class="stat-value">mobius@juspay.in</div> + <div class="stat-title">Last Modified</div> + <div class="stat-value">{format!("{}", &exp.last_modified.format("%d-%m-%Y %H:%M:%S"))}</div> </div> </div> @@ -66,7 +152,7 @@ pub fn experiment_page() -> impl IntoView { <div class="overflow-x-auto"> <table class="table"> <thead> - <tr> + <tr class="bg-base-200"> <th></th> <th>Key</th> <th>Variant-1</th> diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index d37e1de26..687a413c3 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -142,9 +142,9 @@ pub fn ExperimentList() -> impl IntoView { .to_owned(); view! { <Table - _table_style="abc".to_string() + table_style="abc".to_string() rows=data - _key_column="id".to_string() + key_column="id".to_string() columns=table_columns.get() settings=settings /> diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 55d1035ab..7b0d5ae80 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -5,4 +5,3 @@ pub mod Experiment; pub mod ExperimentList; pub mod Home; pub mod NotFound; -pub mod Experiment; From c697e4d7e0878ced480c494ebedba916ca6ac658 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Wed, 6 Dec 2023 13:59:00 +0530 Subject: [PATCH 206/352] feat: testing create form --- .../experiment_form/experiment_form.rs | 25 ++++++++----------- .../components/override_form/override_form.rs | 7 +++--- crates/frontend/tailwind.config.js | 5 +++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 82c73ccd5..767a4632a 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -4,11 +4,10 @@ use crate::components::{ use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; -use crate::types::InputVector; -use leptos::html::Form; use leptos::*; use serde_json::Map; -use web_sys::SubmitEvent; +use wasm_bindgen::JsCast; +use web_sys::{SubmitEvent, HtmlInputElement}; #[component] pub fn ExperimentForm( @@ -21,8 +20,7 @@ pub fn ExperimentForm( // let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (experiment_name, set_experiment_name) = create_signal(name); let (variants, set_variants) = create_signal(variants); - let input_vector: InputVector = vec![(experiment_name, set_experiment_name)]; - let (iv_rs, iv_ws) = create_signal(input_vector); + // let input_vector: InputVector = vec![(experiment_name, set_experiment_name)]; let on_submit = move |ev: SubmitEvent| { ev.prevent_default(); let document = document(); @@ -30,14 +28,13 @@ pub fn ExperimentForm( .get_element_by_id("expName") .expect("expName input not found"); logging::log!("{:#?}", exp_name.get_attribute_names()); - let variant_ids = document.get_elements_by_tag_name("variantId"); - for i in 0..variant_ids.length() { - logging::log!("{:#?}", variant_ids.item(i)); - } - let override_vec = document.get_elements_by_tag_name("override"); - for i in 0..override_vec.length() { - logging::log!("{:#?}", override_vec.item(i)); - } + logging::log!("{:#?}", exp_name.get_attribute("value").unwrap()); + let variant_ids = document.get_elements_by_name("variantId"); + logging::log!("{:#?}", variant_ids.length()); + logging::log!("{:#?}", variant_ids.item(0).expect("missing input").dyn_ref::<HtmlInputElement>().unwrap().value()); + let override_vec = document.get_elements_by_name("override"); + logging::log!("{:#?}", override_vec.length()); + logging::log!("{:#?}", override_vec.item(0).expect("missing override input").dyn_ref::<HtmlInputElement>().unwrap().value()); }; view! { @@ -77,7 +74,7 @@ pub fn ExperimentForm( <span class="label-text">Id</span> </label> <input - name="variantId[]" + name="variantId" value=move || variant_rs.get().id type="text" placeholder="Type a unique name here" diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 8931c9bde..310e29293 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,7 +1,6 @@ use crate::pages::ExperimentList::types::DefaultConfig; use leptos::*; use serde_json::{Map, Value}; -use std::collections::HashSet; #[component] pub fn OverrideForm( @@ -49,13 +48,15 @@ pub fn OverrideForm( <div class="flex gap-x-6 items-center"> <input type="text" - placeholder="Type here" + placeholder="Enter override here" + name="override" value=config_value.to_string() class="input input-bordered w-full bg-white text-gray-700 shadow-md" /> <button class="btn btn-error btn-circle" - on:click=move |_| { + on:click=move |ev| { + ev.prevent_default(); set_overrides .update(|value| { value.remove(&config_key); diff --git a/crates/frontend/tailwind.config.js b/crates/frontend/tailwind.config.js index 5e6d76994..645217149 100644 --- a/crates/frontend/tailwind.config.js +++ b/crates/frontend/tailwind.config.js @@ -8,9 +8,12 @@ module.exports = { "./src/hoc/**/*.rs" ], theme: { - extend: {}, + extend: {}, }, plugins: [ require("daisyui") ], + daisyui: { + themes: ["light", "dark", "cupcake", "dim"], + }, } \ No newline at end of file From 384376388ac18363acdbf8c9da59ed6344678a7c Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 7 Dec 2023 16:19:26 +0530 Subject: [PATCH 207/352] feat: added state changes in the form --- .../components/context_form/context_form.rs | 168 +++++++----- .../experiment_form/experiment_form.rs | 193 +++++++------- .../components/override_form/override_form.rs | 178 +++++++++---- crates/frontend/src/hoc/layout/layout.rs | 2 +- .../pages/ContextOverride/ContextOverride.rs | 243 +++++++++++++----- .../pages/ExperimentList/ExperimentList.rs | 6 +- crates/frontend/src/pages/Home/Home.rs | 206 +-------------- 7 files changed, 516 insertions(+), 480 deletions(-) diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index b58b8748c..87d399633 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -1,31 +1,102 @@ use crate::pages::ExperimentList::types::Dimension; use leptos::*; +use std::cmp; use std::collections::HashSet; +use wasm_bindgen::JsCast; +use web_sys::{HtmlInputElement, HtmlSelectElement}; +use serde_json::json; #[component] pub fn ContextForm( dimensions: Vec<Dimension>, context: Vec<(String, String, String)>, -) -> impl IntoView { +) -> impl IntoView +{ let (context, set_context) = create_signal(context); let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); let total_dimensions = dimensions.len(); + let condition_context = + use_context::<RwSignal<Vec<(String, String, String, String)>>>(); - // please suggest a better way to write this let last_idx = create_memo(move |_| { let len = context.get().len(); - if len == 0 { - 0 - } else { - len - 1 + cmp::max(0, len - 1) + }); + + create_effect(move |_| { + let values = context + .get() + .clone() + .into_iter() + .enumerate() + .map(|(idx, (dimension, operator, value))| { + (idx.to_string(), dimension.to_string(), operator.to_string(), value.to_string()) + }) + .collect::<Vec<(String, String, String, String)>>(); + if let Some(c_context) = condition_context { + c_context.set(values.clone()); } }); view! { <div class="form-control w-full "> - <label class="label"> - <span class="label-text font-semibold text-lg">Context</span> - </label> + <div class="flex gap-4 justify-between"> + <label class="label"> + <span class="label-text font-semibold text-base">Context</span> + </label> + <div> + <div class="dropdown dropdown-left"> + <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> + <i class="ri-add-line"></i> + Add Dimension + </label> + <ul + tabindex="0" + class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" + > + <For + each=move || { + dimensions + .clone() + .into_iter() + .filter(|dim| { + !used_dimensions.get().contains(&dim.dimension) + }) + .collect::<Vec<Dimension>>() + } + + key=|dimension: &Dimension| dimension.dimension.to_string() + children=move |dimension: Dimension| { + let dimension_name = dimension.dimension.to_string(); + let label = dimension_name.to_string(); + view! { + <li on:click=move |_| { + set_context + .update(|value| { + leptos::logging::log!("{:?}", value); + value + .push(( + dimension_name.to_string(), + "".to_string(), + "".to_string(), + )) + }); + set_used_dimensions + .update(|value| { + value.insert(dimension_name.to_string()); + }); + }> + + <a>{label.to_string()}</a> + </li> + } + } + /> + + </ul> + </div> + </div> + </div> <div class="p-4"> <For each=move || { @@ -45,13 +116,23 @@ pub fn ContextForm( <label class="label font-medium font-mono text-sm"> <span class="label-text">Operator</span> </label> - <select class="select select-bordered w-full bg-black text-white text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline"> + <select + bind:value=operator + on:input=move |event| { + let input_value = event_target_value(&event); + set_context.update(|curr_context| { + // setting operator + curr_context[idx].1 = input_value; + }); + } + class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" + > <option disabled selected> Pick one </option> <option value="==">==</option> <option value="!=">!=</option> - <option value="!=">IN</option> + <option value="IN">IN</option> </select> </div> @@ -61,12 +142,19 @@ pub fn ContextForm( </label> <div class="flex gap-x-6 items-center"> <input + bind:value=value + on:input=move |event| { + let input_value = event_target_value(&event); + set_context.update(|curr_context| { + curr_context[idx].2 = input_value; + }); + } type="text" placeholder="Type here" class="input input-bordered w-full bg-white text-gray-700 shadow-md" /> <button - class="btn btn-error btn-circle" + class="btn btn-ghost btn-circle btn-sm" on:click=move |_| { set_context .update(|value| { @@ -78,8 +166,7 @@ pub fn ContextForm( }); } > - - <i class="ri-delete-bin-2-line"></i> + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> </button> </div> </div> @@ -100,58 +187,7 @@ pub fn ContextForm( } } /> - - <div class="mt-2"> - <div class="dropdown"> - <label tabindex="0" class="btn btn-circle btn-info text-white btn-sm m-1"> - <i class="ri-add-line font-bold text-2xl"></i> - </label> - <ul - tabindex="0" - class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" - > - <For - each=move || { - dimensions - .clone() - .into_iter() - .filter(|dim| { - !used_dimensions.get().contains(&dim.dimension) - }) - .collect::<Vec<Dimension>>() - } - - key=|dimension: &Dimension| dimension.dimension.to_string() - children=move |dimension: Dimension| { - let dimension_name = dimension.dimension.to_string(); - let label = dimension_name.to_string(); - view! { - <li on:click=move |_| { - set_context - .update(|value| { - value - .push(( - dimension_name.to_string(), - "".to_string(), - "".to_string(), - )) - }); - set_used_dimensions - .update(|value| { - value.insert(dimension_name.to_string()); - }); - }> - - <a>{label.to_string()}</a> - </li> - } - } - /> - - </ul> - </div> - </div> </div> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 767a4632a..0406b43b6 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -41,7 +41,7 @@ pub fn ExperimentForm( <form on:submit=on_submit> <div class="form-control w-full"> <label class="label"> - <span class="label-text">Experiment Name</span> + <span class="label-text">Name</span> </label> <input value=move || experiment_name.get() @@ -49,101 +49,116 @@ pub fn ExperimentForm( type="text" name="expName" id="expName" - placeholder="ex. testing hyperpay release" + placeholder="ex: testing hyperpay release" class="input input-bordered w-full max-w-xs" /> </div> - <ContextForm dimensions=dimensions context=context/> - - <div> - <label> - <span class="label-text">Variants</span> - </label> + <div class="my-4"> + <ContextForm dimensions=dimensions context=context/> </div> - <For - each=move || variants.get() - key=|variant: &Variant| variant.id.to_string() - children=move |variant: Variant| { - let config = default_config.clone(); - let (variant_rs, variant_ws) = create_signal(variant); - view! { - <div> - <div class="form-control w-full"> - <label class="label"> - <span class="label-text">Id</span> - </label> - <input - name="variantId" - value=move || variant_rs.get().id - type="text" - placeholder="Type a unique name here" - class="input w-full max-w-xs" - /> - </div> - <div class="form-control w-full"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=move || variant_rs.get().variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_rs.get().clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - variant_ws.set(new_variant); - } - class="select select-bordered" - > - <option disabled selected> - Pick one - </option> - <option value=VariantType::CONTROL - .to_string()>{VariantType::CONTROL.to_string()}</option> - <option value=VariantType::EXPERIMENTAL - .to_string()> - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> - </div> - <div> - <OverrideForm - overrides=variant_rs.get().overrides - default_config=config - /> - </div> - </div> - } - } - /> - // make some more random default id for this can lead to undefined behaviour if we use index based ids - <button - class="btn btn-circle btn-outline" - on:click=move |_| { - leptos::logging::log!("add new variant"); - set_variants - .update(|value| { - let total_variants = value.len(); - value - .push(Variant { - id: format!("variant-{}", total_variants), - variant_type: VariantType::EXPERIMENTAL, - context_id: None, - override_id: None, - overrides: Map::new(), - }) - }); - } - > + <div class="form-control w-full"> + <div class="flex items-center justify-between gap-4"> + <label class="label"> + <span class="label-text font-semibold text-base">Variants</span> + </label> - <i class="ri-add-circle-fill"></i> - </button> + // make some more random default id for this can lead to undefined behaviour if we use index based ids + <button + class="btn btn-outline btn-sm text-xs m-1" + on:click=move |_| { + leptos::logging::log!("add new variant"); + set_variants + .update(|value| { + let total_variants = value.len(); + value + .push(Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + }) + }); + } + > + <i class="ri-add-line"></i> + Add Variant + </button> + </div> + <For + each=move || { + variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, Variant)>>() + } + key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) + children=move |(idx, variant)| { + let config = default_config.clone(); + let variant_clone = variant.clone(); + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=move || variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants.update(|value| { + value[idx] = new_variant; + }) + } + + class="select select-bordered" + > + <option disabled selected> + Pick one + </option> + <option value=VariantType::CONTROL + .to_string()>{VariantType::CONTROL.to_string()}</option> + <option value=VariantType::EXPERIMENTAL + .to_string()> + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> + </div> + </div> + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=config + /> + </div> + </div> + } + } + /> + </div> <div class="modal-action"> <form method="dialog"> <button class="btn">Save</button> @@ -152,4 +167,4 @@ pub fn ExperimentForm( </form> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 310e29293..93de3072a 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,72 +1,152 @@ use crate::pages::ExperimentList::types::DefaultConfig; use leptos::*; -use serde_json::{Map, Value}; +use serde_json::{Map, Value, json}; +use std::collections::HashSet; +use wasm_bindgen::JsCast; +use web_sys::{HtmlInputElement, HtmlSelectElement}; #[component] pub fn OverrideForm( overrides: Map<String, Value>, default_config: Vec<DefaultConfig>, ) -> impl IntoView { - let (overrides, set_overrides) = create_signal(overrides); + let has_default_config = default_config.len() != 0; + let (default_overrides, default_used_config_keys): (Map<String, Value>, Vec<String>) = if overrides.len() == 0 && has_default_config { + ( + Map::from_iter([(default_config[0].key.to_string(), json!(""))]), + vec![default_config[0].key.to_string()] + ) + } else { + ( + overrides.clone(), + overrides.keys().map(String::from).collect::<Vec<String>>() + ) + }; + let (overrides, set_overrides) = create_signal(default_overrides); + let (used_config_keys, set_used_config_keys) = create_signal(HashSet::from_iter(default_used_config_keys)); + + let override_signal = use_context::<RwSignal<(String, Map<String, Value>)>>(); + + create_effect(move |_| { + let overrides_vec = overrides.get().clone(); + let overrides_map: Map<String, Value> = overrides_vec.into_iter().collect(); + + if let Some(override_context) = override_signal { + override_context.set(( + "SomeName".to_string(), // Adjust according to your needs + overrides_map, + )); + } + }); + view! { - <div class="space-y-4 "> - <label class="label"> - <span class="label-text font-semibold text-lg">Override</span> - </label> + <div class="space-y-4"> + <div class="flex items-center justify-between gap-4"> + <label class="label"> + <span class="label-text font-semibold text-base">Override</span> + </label> + <div> + <div class="dropdown dropdown-left"> + <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> + <i class="ri-add-line"></i> + Add Config Key + </label> + <ul + tabindex="0" + class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" + > + <Show + when = move || !has_default_config + > + No default config + </Show> + <For + each=move || { + default_config + .clone() + .into_iter() + .filter(|item| { + !used_config_keys.get().contains(&item.key) + }) + .collect::<Vec<DefaultConfig>>() + } + + key=|item: &DefaultConfig| item.key.to_string() + children=move |item: DefaultConfig| { + let config_key = item.key.to_string(); + let label = config_key.to_string(); + view! { + <li on:click=move |_| { + set_overrides + .update(|value| { + value + .insert( + config_key.to_string(), + json!("") + ); + }); + set_used_config_keys + .update(|value: &mut HashSet<String>| { + value.insert(config_key.to_string()); + }); + }> + + <a>{label.to_string()}</a> + </li> + } + } + /> + + </ul> + </div> + </div> + </div> <For each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } key=|(config_key, _)| config_key.to_string() children=move |(config_key, config_value)| { - let configs = default_config.clone(); - let config_key_copy = config_value.clone(); + let config_key_label = config_key.to_string(); + let config_key_value = config_key.to_string(); view! { - <div class="card bg-base-100 shadow-xl"> - <div class="card-body"> - <div class="flex items-center gap-4"> - <select class="select select-bordered w-full bg-gray-200 text-white-400/50"> - <option disabled selected class="text-sky-400/50 font-mono"> - Select Config - </option> - <For - each=move || configs.clone() - key=|item: &DefaultConfig| item.key.to_string() - children=move |item: DefaultConfig| { - view! { - <option - value=item.key.to_string() - selected=item.key.to_string() == config_key_copy - > - {item.key} - </option> - } - } - /> - - </select> + <div> + <div class="flex items-center gap-4"> + <div class="form-control"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">{config_key_label}":"</span> + </label> </div> - <div class="form-control flex-grow"> - <div class="flex gap-x-6 items-center"> + <div class="form-control w-2/5"> <input type="text" placeholder="Enter override here" name="override" - value=config_value.to_string() class="input input-bordered w-full bg-white text-gray-700 shadow-md" - /> - <button - class="btn btn-error btn-circle" - on:click=move |ev| { - ev.prevent_default(); - set_overrides - .update(|value| { - value.remove(&config_key); - }); + bind:value=config_value.to_string() + on:input=move |event| { + let input_value = event_target_value(&event); + // TODO: validations + set_overrides.update(|curr_overrides| { + curr_overrides.insert(config_key_value.to_string(), json!(input_value)); + }); } - > - - <i class="ri-delete-bin-2-line"></i> - </button> - </div> + /> + </div> + <div class="w-1/5"> + <button + class="btn btn-ghost btn-circle btn-sm" + on:click=move |ev| { + ev.prevent_default(); + set_overrides + .update(|value| { + value.remove(&config_key); + }); + set_used_config_keys.update(|value| { + value.remove(&config_key); + }); + } + > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> </div> </div> </div> @@ -76,4 +156,4 @@ pub fn OverrideForm( </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 0e1c3e45f..73e19260d 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -6,7 +6,7 @@ pub fn Layout(children: Children) -> impl IntoView { view! { <div> <SideNav/> - <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200"> + <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200 overflow-y-auto"> {children()} </main> </div> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 860d49f92..1f735060a 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -12,7 +12,7 @@ use leptos::ev::SubmitEvent; use leptos::svg::view; use leptos::*; use leptos_router::use_query_map; -use serde_json::{Map, Value}; +use serde_json::{json, Map, Value}; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); @@ -27,32 +27,7 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { } } -// pub async fn create_context( -// tenant: String, -// key: String, -// value: String, -// key_type: String, -// pattern: String, -// ) -> Result<String, String> { -// let client = reqwest::Client::new(); -// let host = "http://localhost:8080"; -// let url = format!("{host}/context"); -// let mut req_body: HashMap<&str, Value> = HashMap::new(); -// let mut schema: Map<String, Value> = Map::new(); -// schema.insert("type".to_string(), Value::String(key_type)); -// schema.insert("pattern".to_string(), Value::String(pattern)); -// req_body.insert("value", Value::String(value)); -// req_body.insert("schema", Value::Object(schema)); -// let response = client -// .put(url) -// .header("x-tenant", tenant) -// .header("Authorization", "Bearer 12345678") -// .json(&req_body) -// .send() -// .await -// .map_err(|e| e.to_string())?; -// response.text().await.map_err(|e| e.to_string()) -// } + pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); @@ -82,8 +57,6 @@ fn ContextModalForm() -> impl IntoView { let dimensions = create_blocking_resource(|| {}, move |_| fetch_dimensions(tenant.clone())); - let context = use_context::<RwSignal<Vec<(String, String, String)>>>(); - view! { <div> <Suspense fallback=move || { @@ -99,7 +72,7 @@ fn ContextModalForm() -> impl IntoView { <div> <ContextForm dimensions=dimension.clone() - context=context.unwrap().get().clone() + context=vec![] /> </div> } @@ -136,6 +109,69 @@ pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, } } +pub fn construct_request_payload( + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, +) -> Value { + // Construct the override section + let override_section: Map<String, Value> = overrides; + + // Construct the context section + let context_section = if conditions.len() == 1 { + // Single condition + let (variable, operator, value) = &conditions[0]; + json!({ + operator: [ + { "var": variable }, + value + ] + }) + } else { + // Multiple conditions inside an "and" + let and_conditions: Vec<Value> = conditions + .into_iter() + .map(|(variable, operator, value)| { + json!({ + operator: [ + { "var": variable }, + value + ] + }) + }) + .collect(); + + json!({ "and": and_conditions }) + }; + + // Construct the entire request payload + let request_payload = json!({ + "override": override_section, + "context": context_section + }); + + request_payload +} + +pub async fn create_context( + tenant: String, + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/context"); + let request_payload = construct_request_payload(overrides, conditions); + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + response.text().await.map_err(|e| e.to_string()) +} + #[component] fn OverrideModalForm() -> impl IntoView { let query = use_query_map(); @@ -151,8 +187,6 @@ fn OverrideModalForm() -> impl IntoView { let default_config = create_blocking_resource(|| {}, move |_| fetch_default_config(tenant.clone())); - let override_data = use_context::<RwSignal<Map<String, Value>>>(); - view! { <div> <Suspense fallback=move || { @@ -167,7 +201,7 @@ fn OverrideModalForm() -> impl IntoView { view! { <div> <OverrideForm - overrides=override_data.unwrap().get().clone() + overrides=Map::new() default_config=config.clone() /> </div> @@ -258,43 +292,118 @@ fn format_condition(condition: &Value) -> String { "Invalid Condition".to_string() } -// Vec[Condition] -> Update it. -// Vec[Override] -> Update it. - #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { + let context_data: Vec<(String, String, String, String)> = vec![]; + let condition_ctx: RwSignal<Vec<(String, String, String, String)>> = + create_rw_signal(context_data); + + provide_context(condition_ctx); + + let ovrride_signal: RwSignal<(String, Map<String, Value>)> = + create_rw_signal(("str".to_string(), Map::new())); + + provide_context(ovrride_signal); + + let (ctx_form, set_ctx_form) = create_signal(vec![( + "dimension1".to_string(), + "operator1".to_string(), + "value1".to_string(), + "string".to_string(), + )]); + + let (ovrride_form, set_ovrride_form) = + create_signal(("dummy".to_string(), Map::new())); + + let on_submit = { + move |ev: SubmitEvent| { + let handle_submit_clone = handle_submit.clone(); + ev.prevent_default(); + set_ctx_form.set(condition_ctx.get()); + let ovrd_values = ovrride_signal.get().1; + // set_ovrride_form.set(ovrride_signal.get()) + set_ovrride_form.set(( + "Override Name".to_string(), + ovrd_values + .into_iter() + .filter_map(|(key, value)| { + let value_clone = value.clone(); + if let Value::String(val) = value { + if !val.is_empty() { + Some((key, value_clone)) + } else { + None + } + } else { + None + } + }) + .collect::<Map<String, Value>>(), + )); + + let context_tuples: Vec<(String, String, String)> = ctx_form + .get() + .iter() + .map(|(_dim, dim, op, val)| (dim.clone(), op.clone(), val.clone())) + .collect(); + + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_context( + "mjos".to_string(), + ovrride_form.get().1, + context_tuples, + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(_) => { + // Handle error + // We can consider logging or displaying the error + } + } + } + }); + } + }; + view! { - <div class="p-6 text-gray-600 space-y-6"> - <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> - Create Context Overrides - <i class="ri-edit-2-line ml-2"></i> - </button> - // - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill"></i> - </button> - </form> - // on:submit=on_submit - <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> - <ContextModalForm/> - <OverrideModalForm/> - <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" - > - Submit + <div class="p-6 text-gray-600 space-y-6"> + <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> + Create Context Overrides + <i class="ri-edit-2-line ml-2"></i> + </button> + // + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> </button> - </div> - </form> - </div> - </dialog> - </div> - } + </form> + // on:submit=on_submit + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit> + <ContextModalForm/> + <OverrideModalForm/> + <div class="form-control mt-6"> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit + </button> + </div> + + </form> + </div> + </dialog> + </div> + } } fn parse_conditions(input: String) -> Vec<(String, String, String)> { @@ -457,4 +566,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 687a413c3..67bf5b548 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -203,8 +203,8 @@ pub fn ExperimentList() -> impl IntoView { let def_conf = default_config.get().unwrap_or(vec![]); view! { <dialog id="create_exp_modal" class="modal"> - <div class="modal-box"> - <h3 class="font-bold text-lg">Create an Experiment</h3> + <div class="modal-box w-12/12 max-w-5xl"> + <h3 class="font-bold text-lg">Create Experiment</h3> <div class="modal-action flex flex-col"> <ExperimentForm name="".to_string() @@ -222,4 +222,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 8527a501d..eaa6018ad 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -1,212 +1,8 @@ use leptos::*; -use leptos_router::use_query_map; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; - -#[derive(Deserialize, Serialize, Clone)] -pub struct Config { - pub contexts: Vec<Context>, - pub overrides: Map<String, Value>, - pub default_configs: Map<String, Value>, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Context { - pub id: String, - pub condition: Value, - pub override_with_keys: [String; 1], -} - -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; - let url = format!("{host}/config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} - -pub fn extract_and_format(condition: &Value) -> String { - if condition.is_object() && condition.get("and").is_some() { - // Handling complex "and" conditions - let empty_vec = vec![]; - let conditions_json = condition - .get("and") - .and_then(|val| val.as_array()) - .unwrap_or(&empty_vec); // Default to an empty vector if not an array - - let mut formatted_conditions = Vec::new(); - for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); - } - - formatted_conditions.join(" and ") - } else { - // Handling single conditions - format_condition(condition) - } -} - -fn format_condition(condition: &Value) -> String { - if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { - let empty_vec = vec![]; - let operands = condition[operator].as_array().unwrap_or(&empty_vec); - - // Handling the "in" operator differently - if operator.as_str() == "in" { - let left_operand = &operands[0]; - let right_operand = &operands[1]; - - let left_str = if left_operand.is_string() { - format!("\"{}\"", left_operand.as_str().unwrap()) - } else { - format!("{}", left_operand) - }; - - if right_operand.is_object() && right_operand["var"].is_string() { - let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); - } - } - - // Handling regular operators - if let Some(first_operand) = operands.get(0) { - if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); - if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } - } - } - } - } - - "Invalid Condition".to_string() -} #[component] pub fn Home() -> impl IntoView { - let query = use_query_map(); - - let tenant = - query.with(|params_map| params_map.get("tenant").cloned().unwrap_or_default()); - let config_data = - create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); - view! { - <div class="container mt-5" > - <div class="text-center mb-4"> - <h3 class="fw-bold">"Welcome to Context Aware Config!"</h3> - </div> - <Suspense fallback=move || view! {<p>"Loading (Suspense Fallback)..."</p> }> - { - config_data.with(move |result| { - match result { - Some(Ok(config)) => { - let rows = |k:&String, v:&Value| { - let key = k.replace("\"", "").trim().to_string(); - let value = format!("{}", v).replace("\"", "").trim().to_string(); - view! { - <tr> - <td class="fw-normal col w-50 shadow-sm"> <div class ="col"> {key}</div></td> - <td class="fw-normal col w-50 shadow-sm"><div class ="col">{value}</div></td> - </tr> - } - }; - - let contexts_views: Vec<_> = config.contexts.iter().map(|context| { - let condition = extract_and_format(&context.condition); - let rows: Vec<_> = context.override_with_keys.iter() - .filter_map(|key| config.overrides.get(key)) - .flat_map(|ovr| ovr.as_object().unwrap().iter()) - .map(|(k, v)| { - rows(&k,&v) - }).collect(); - - view! { - <h6 class="fw-normal font-monospace">"Condition: " <span class="badge rounded-pill bg-secondary small"> {&condition} </span> </h6> - <table class="table table-responsive table-bordered table-hover border-secondary"> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody class="bg-light"> - { rows } - </tbody> - </table> - } - }).collect::<Vec<_>>(); - - let new_context_views = contexts_views.into_iter().rev().collect::<Vec<_>>(); - let default_config: Vec<_> = config.default_configs.iter().map(|(k,v)|{ - rows(&k,&v) - }).collect(); - - vec![ - view! { - <div class="mb-4 "> - { new_context_views } - <h6 class="mb-3 f-6 fw-normal font-monospace">"Default Configuration"</h6> - <table class="table table-responsive table-striped table-bordered table-hover border-secondary "> - <thead class="table-primary border-secondary"> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody> - {default_config} - </tbody> - </table> - </div> - } - ] - }, - Some(Err(error)) => { - vec![ - view! { - <div class="error"> - {"Failed to fetch config data: "} - {error} - </div> - } - ] - }, - None => { - vec![ - view! { - <div class="error"> - {"No config data fetched"} - </div> - } - ] - } - } - }) - } - </Suspense> - </div> - - + <h1> Welcome to Context Aware Config </h1> } } From b04d3f500dfd953a1b1cd59c5dee1e83ad83abfa Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Fri, 8 Dec 2023 11:46:40 +0530 Subject: [PATCH 208/352] fix: dimensions page updates --- .../src/pages/Dimensions/Dimensions.rs | 252 +++++++++++++++++- 1 file changed, 248 insertions(+), 4 deletions(-) diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index e4f8aed05..fb93209f9 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,30 +1,274 @@ +use std::collections::HashMap; +use std::rc::Rc; + use leptos::logging::*; use leptos::*; use serde_json::{json, Map, Value}; +use web_sys::SubmitEvent; use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; use crate::pages::Dimensions::helper::fetch_dimensions; +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub dimension: String, + pub priority: String, + pub type_: String, + pub pattern: String +} + +pub fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { + let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); + let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); + let row_priority = row["priority"].clone().to_string().replace("\"", ""); + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<serde_json::Value>(&schema).unwrap(); + + let row_type = schema_object.get("type").unwrap().to_string(); + let row_pattern = schema_object.get("pattern").unwrap().to_string(); + + let edit_click_handler = move |_| { + let row_data = RowData { + dimension: row_dimension.clone(), + priority: row_priority.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + }; + intermediate_signal.set(Some(row_data)); + js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); + }; + + let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + + view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() +} + +pub async fn create_dimension( + tenant: String, + key: String, + priority: String, + key_type: String, + pattern: String, +) -> Result<String, String> { + let priority: i64 = priority.parse().unwrap(); + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/dimension"); + + let mut req_body: HashMap<&str, Value> = HashMap::new(); + let mut schema: Map<String, Value> = Map::new(); + + schema.insert("type".to_string(), Value::String(key_type.replace("\"", ""))); + schema.insert("pattern".to_string(), Value::String(pattern.replace("\"", ""))); + + req_body.insert("dimension", Value::String(key)); + req_body.insert("priority", Value::Number(priority.into())); + req_body.insert("schema", Value::Object(schema)); + + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&req_body) + .send() + .await + .map_err(|e| e.to_string())?; + response.text().await.map_err(|e| e.to_string()) +} + +#[component] +fn ModalComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> impl IntoView { + view! { + <div class="pt-4"> + <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> + Create Dimension + <i class="ri-edit-2-line ml-2"></i> + </button> + <FormComponent handle_submit=handle_submit tenant=tenant/> + </div> + } +} + +#[component] +fn FormComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> impl IntoView { + use leptos::html::Input; + let handle_submit = handle_submit.clone(); + let global_state = use_context::<RwSignal<RowData>>(); + let row_data = global_state.unwrap().get(); + + let (dimension, set_dimension) = create_signal(row_data.dimension); + let (priority, set_priority) = create_signal(row_data.priority); + let (keytype, set_keytype) = create_signal(row_data.type_); + let (pattern, set_pattern) = create_signal(row_data.pattern); + + create_effect(move |_| { + if let Some(row_data) = global_state { + set_dimension.set(row_data.get().dimension.clone().to_string()); + set_priority.set(row_data.get().priority.clone()); + set_keytype.set(row_data.get().type_.clone().to_string()); + set_pattern.set(row_data.get().pattern.clone()); + } + }); + + let input_element: NodeRef<Input> = create_node_ref(); + let input_element_two: NodeRef<Input> = create_node_ref(); + let input_element_three: NodeRef<Input> = create_node_ref(); + let input_element_four: NodeRef<Input> = create_node_ref(); + + let on_submit = { + let handle_submit = handle_submit.clone(); + move |ev: SubmitEvent| { + ev.prevent_default(); + + let value1 = input_element.get().expect("<input> to exist").value(); + let value2 = input_element_two.get().expect("<input> to exist").value(); + let value3 = input_element_three.get().expect("<input> to exist").value(); + let value4 = input_element_four.get().expect("<input> to exist").value(); + + set_dimension.set(value1.clone()); + set_priority.set(value2.clone()); + set_keytype.set(value3.clone()); + set_pattern.set(value4.clone()); + let handle_submit_clone = handle_submit.clone(); + + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_dimension( + tenant.get(), + dimension.get(), + priority.get(), + keytype.get(), + pattern.get(), + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(_) => { + // Handle error + // Consider logging or displaying the error + } + } + } + }); + } + }; + + view! { + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill" onclick="my_modal_5.close()"></i> + </button> + </form> + <form + class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" + on:submit=on_submit + > + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Dimension</span> + </label> + <input + type="text" + placeholder="Dimension" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=dimension + node_ref=input_element + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Type</span> + </label> + <input + type="text" + placeholder="Type" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=keytype + node_ref=input_element_three + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> + </label> + <input + type="text" + placeholder="Pattern" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=pattern + node_ref=input_element_four + /> + </div> + <div class="form-control mt-6"> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit + </button> + </div> + </form> + </div> + </dialog> + } +} + + #[component] pub fn Dimensions() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let global_state = create_rw_signal(RowData::default()); + provide_context(global_state); + + let intermediate_signal = create_rw_signal(None::<RowData>); + + create_effect(move |_| { + if let Some(row_data) = intermediate_signal.get() { + global_state.set(row_data.clone()); + } + }); + + provide_context(intermediate_signal.clone()); + let dimensions = create_blocking_resource( move || {}, |_value| async move { match fetch_dimensions().await { Ok(data) => data, - Err(e) => vec![], + Err(_) => vec![], } }, ); let table_columns = create_memo(move |_| { vec![ - Column::default("created_at".to_string()), - Column::default("created_by".to_string()), Column::default("dimension".to_string()), Column::default("priority".to_string()), Column::default("schema".to_string()), + Column::default("created_by".to_string()), + Column::default("created_at".to_string()), + Column::new("EDIT".to_string(), None, Some(custom_formatter)), ] }); @@ -50,6 +294,7 @@ pub fn Dimensions() -> impl IntoView { </div> </div> + <ModalComponent handle_submit=Rc::new(move || dimensions.refetch()) tenant=tenant_rs/> </div> <div class="card rounded-xl w-full bg-base-100 shadow"> @@ -69,7 +314,6 @@ pub fn Dimensions() -> impl IntoView { .map(|ele| { json!(ele).as_object().unwrap().clone() }) .collect::<Vec<Map<String, Value>>>() .to_owned(); - println!("hello1: {:?}", data); view! { <Table table_style="abc".to_string() From c651e4adb196552329adbbc21d392be7438d9c66 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Fri, 8 Dec 2023 13:42:16 +0530 Subject: [PATCH 209/352] feat: working resolve page --- crates/frontend/src/api.rs | 71 +++ crates/frontend/src/app.rs | 21 +- .../components/context_form/context_form.rs | 26 +- .../experiment_form/experiment_form.rs | 129 ++-- .../src/components/nav_item/nav_item.rs | 6 +- .../components/override_form/override_form.rs | 50 +- crates/frontend/src/components/table/table.rs | 5 +- crates/frontend/src/lib.rs | 1 + .../pages/ContextOverride/ContextOverride.rs | 105 ++-- .../src/pages/DefaultConfig/DefaultConfig.rs | 4 +- .../src/pages/Dimensions/Dimensions.rs | 147 ++--- crates/frontend/src/pages/Experiment/mod.rs | 577 ++++++++++++++---- crates/frontend/src/pages/Home/Home.rs | 384 +++++++++++- .../frontend/src/pages/NotFound/NotFound.rs | 4 +- 14 files changed, 1150 insertions(+), 380 deletions(-) create mode 100644 crates/frontend/src/api.rs diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs new file mode 100644 index 000000000..1b6ba6e3a --- /dev/null +++ b/crates/frontend/src/api.rs @@ -0,0 +1,71 @@ +use leptos::*; + +use crate::pages::ExperimentList::types::{Dimension, DefaultConfig}; + +// #[derive(Debug, Serialize, Deserialize, Clone)] +// pub struct Dimension { +// pub dimension: String, +// pub priority: i32, +// pub created_at: DateTime<Utc>, +// pub created_by: String, +// pub schema: Value, +// } + +// #[derive(Serialize, Deserialize, Clone, Debug)] +// pub struct DefaultConfig { +// pub key: String, +// pub value: Value, +// pub created_at: DateTime<Utc>, +// pub created_by: String, +// pub schema: Value, +// } + +pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/dimension"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let dimensions = response.json().await.map_err(|e| e.to_string())?; + Ok(dimensions) + } + Err(e) => Err(e.to_string()), + } +} + +pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/default-config"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let default_config = response.json().await.map_err(|e| e.to_string())?; + Ok(default_config) + } + Err(e) => Err(e.to_string()), + } +} + +pub fn dimension_resource(tenant: ReadSignal<String>) -> Resource<String, Vec<Dimension>> { + create_blocking_resource( + move || tenant.get(), + |tenant| async { + match fetch_dimensions(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ) +} + +pub fn default_config_resource(tenant: ReadSignal<String>) -> Resource<String, Vec<DefaultConfig>> { + create_blocking_resource( + move || tenant.get(), + |tenant| async { + match fetch_default_config(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ) +} diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index c53bbec5e..d1edcb1ef 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -18,8 +18,6 @@ pub fn App() -> impl IntoView { provide_context(tenant_rs); provide_context(tenant_ws); view! { - // injects a stylesheet into the document <head> - // id=leptos means cargo-leptos will hot-reload this stylesheet <Stylesheet id="leptos" href="/pkg/style.css"/> // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> @@ -50,12 +48,21 @@ pub fn App() -> impl IntoView { view=ExperimentPage /> <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> - <Route ssr=SsrMode::PartiallyBlocked path="/admin/:tenant/default-config" view=DefaultConfig/> <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/overrides" - view=ContextOverride - /> + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/default-config" + view=DefaultConfig + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/overrides" + view=ContextOverride + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/resolve" + view=Home + /> <Route path="/*any" view=NotFound/> </Routes> diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 87d399633..b1738ec84 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -106,6 +106,7 @@ pub fn ContextForm( .enumerate() .collect::<Vec<(usize, (String, String, String))>>() } + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) children=move |(idx, (dimension, operator, value))| { let dimension_label = dimension.to_string(); @@ -120,11 +121,13 @@ pub fn ContextForm( bind:value=operator on:input=move |event| { let input_value = event_target_value(&event); - set_context.update(|curr_context| { - // setting operator - curr_context[idx].1 = input_value; - }); + set_context + .update(|curr_context| { + curr_context[idx].1 = input_value; + }); } + + name="context-dimension-operator" class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" > <option disabled selected> @@ -138,17 +141,22 @@ pub fn ContextForm( </div> <div class="form-control"> <label class="label capitalize font-mono text-sm"> - <span class="label-text">{dimension_label}</span> + <span name="context-dimension-name" class="label-text"> + {dimension_label} + </span> </label> <div class="flex gap-x-6 items-center"> <input bind:value=value on:input=move |event| { let input_value = event_target_value(&event); - set_context.update(|curr_context| { - curr_context[idx].2 = input_value; - }); + set_context + .update(|curr_context| { + curr_context[idx].2 = input_value; + }); } + + name="context-dimension-value" type="text" placeholder="Type here" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -166,6 +174,7 @@ pub fn ContextForm( }); } > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> </button> </div> @@ -187,6 +196,7 @@ pub fn ContextForm( } } /> + </div> </div> } diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 0406b43b6..1f556253b 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -58,7 +58,6 @@ pub fn ExperimentForm( <ContextForm dimensions=dimensions context=context/> </div> - <div class="form-control w-full"> <div class="flex items-center justify-between gap-4"> <label class="label"> @@ -84,80 +83,80 @@ pub fn ExperimentForm( }); } > + <i class="ri-add-line"></i> Add Variant </button> </div> - <For - each=move || { - variants - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, Variant)>>() - } - key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) - children=move |(idx, variant)| { - let config = default_config.clone(); - let variant_clone = variant.clone(); - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=move || variant.variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants.update(|value| { + <For + each=move || { + variants.get().into_iter().enumerate().collect::<Vec<(usize, Variant)>>() + } + + key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) + children=move |(idx, variant)| { + let config = default_config.clone(); + let variant_clone = variant.clone(); + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=move || variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants + .update(|value| { value[idx] = new_variant; }) - } + } - class="select select-bordered" - > - <option disabled selected> - Pick one - </option> - <option value=VariantType::CONTROL - .to_string()>{VariantType::CONTROL.to_string()}</option> - <option value=VariantType::EXPERIMENTAL - .to_string()> - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> - </div> - </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=config - /> + class="select select-bordered" + > + <option disabled selected> + Pick one + </option> + <option value=VariantType::CONTROL + .to_string()>{VariantType::CONTROL.to_string()}</option> + <option value=VariantType::EXPERIMENTAL + .to_string()> + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> </div> </div> - } + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=config + /> + </div> + </div> } - /> + } + /> + </div> <div class="modal-action"> <form method="dialog"> diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 142182108..4ca872413 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -23,9 +23,9 @@ pub fn NavItem( }; view! { - <A href={href} class={anchor_class}> - <div class={icon_wrapper_class}> - <i class={icon_class} /> + <A href=href class=anchor_class> + <div class=icon_wrapper_class> + <i class=icon_class></i> </div> <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 93de3072a..d169b6aea 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -55,11 +55,7 @@ pub fn OverrideForm( tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" > - <Show - when = move || !has_default_config - > - No default config - </Show> + <Show when=move || !has_default_config>No default config</Show> <For each=move || { default_config @@ -79,11 +75,7 @@ pub fn OverrideForm( <li on:click=move |_| { set_overrides .update(|value| { - value - .insert( - config_key.to_string(), - json!("") - ); + value.insert(config_key.to_string(), json!("")); }); set_used_config_keys .update(|value: &mut HashSet<String>| { @@ -112,24 +104,26 @@ pub fn OverrideForm( <div class="flex items-center gap-4"> <div class="form-control"> <label class="label font-medium font-mono text-sm"> - <span class="label-text">{config_key_label}":"</span> + <span class="label-text">{config_key_label} ":"</span> </label> </div> <div class="form-control w-2/5"> - <input - type="text" - placeholder="Enter override here" - name="override" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - bind:value=config_value.to_string() - on:input=move |event| { - let input_value = event_target_value(&event); - // TODO: validations - set_overrides.update(|curr_overrides| { - curr_overrides.insert(config_key_value.to_string(), json!(input_value)); + <input + type="text" + placeholder="Enter override here" + name="override" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + bind:value=config_value.to_string() + on:input=move |event| { + let input_value = event_target_value(&event); + set_overrides + .update(|curr_overrides| { + curr_overrides + .insert(config_key_value.to_string(), json!(input_value)); }); - } - /> + } + /> + </div> <div class="w-1/5"> <button @@ -140,11 +134,13 @@ pub fn OverrideForm( .update(|value| { value.remove(&config_key); }); - set_used_config_keys.update(|value| { - value.remove(&config_key); - }); + set_used_config_keys + .update(|value| { + value.remove(&config_key); + }); } > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> </button> </div> diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index 1890ecef1..bf97f68da 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -89,7 +89,10 @@ pub fn Table( let value: String = generate_table_row_str( row.get(cname).unwrap_or(&Value::String("".to_string())), ); - view! { <td class=table_style.to_string()>{(column.formatter)(&value, &row)}</td> } + view! { + <td class=table_style + .to_string()>{(column.formatter)(&value, &row)}</td> + } }) .collect_view()} diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index b049e5bbb..21989618e 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -3,6 +3,7 @@ pub mod components; pub mod hoc; pub mod pages; pub mod types; +mod api; use cfg_if::cfg_if; cfg_if! { diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 1f735060a..88854acfc 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,18 +1,17 @@ // use std::collections::HashMap; use std::rc::Rc; +use crate::api::{fetch_dimensions, fetch_default_config}; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; use crate::pages::DefaultConfig::types::Config; -use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; -use leptos::ev::SubmitEvent; // use leptos::spawn_local; -use leptos::svg::view; use leptos::*; use leptos_router::use_query_map; use serde_json::{json, Map, Value}; +use web_sys::SubmitEvent; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); @@ -29,18 +28,7 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { -pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/dimension"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let dimensions = response.json().await.map_err(|e| e.to_string())?; - Ok(dimensions) - } - Err(e) => Err(e.to_string()), - } -} + #[component] fn ContextModalForm() -> impl IntoView { @@ -70,10 +58,7 @@ fn ContextModalForm() -> impl IntoView { Some(Ok(dimension)) => { view! { <div> - <ContextForm - dimensions=dimension.clone() - context=vec![] - /> + <ContextForm dimensions=dimension.clone() context=vec![]/> </div> } } @@ -96,18 +81,7 @@ fn ContextModalForm() -> impl IntoView { } } -pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, String> { - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/default-config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let default_config = response.json().await.map_err(|e| e.to_string())?; - Ok(default_config) - } - Err(e) => Err(e.to_string()), - } -} + pub fn construct_request_payload( overrides: Map<String, Value>, @@ -372,38 +346,41 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { }; view! { - <div class="p-6 text-gray-600 space-y-6"> - <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> - Create Context Overrides - <i class="ri-edit-2-line ml-2"></i> - </button> - // - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill"></i> + <div class="p-6 text-gray-600 space-y-6"> + <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> + Create Context Overrides + <i class="ri-edit-2-line ml-2"></i> + </button> + // + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + // on:submit=on_submit + <form + class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" + on:submit=on_submit + > + <ContextModalForm/> + <OverrideModalForm/> + <div class="form-control mt-6"> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit </button> - </form> - // on:submit=on_submit - <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit> - <ContextModalForm/> - <OverrideModalForm/> - <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" - > - Submit - </button> - </div> - - </form> - </div> - </dialog> - </div> - } + </div> + + </form> + </div> + </dialog> + </div> + } } fn parse_conditions(input: String) -> Vec<(String, String, String)> { @@ -483,7 +460,7 @@ pub fn ContextOverride() -> impl IntoView { Some(Ok(config)) => { let mut contexts: Vec<Map<String, Value>> = Vec::new(); let settings = TableSettings { - redirect_prefix: None + redirect_prefix: None, }; let mut context_views = Vec::new(); let mut new_ctx: Vec<(String, String, String)> = vec![]; @@ -534,7 +511,7 @@ pub fn ContextOverride() -> impl IntoView { rows=contexts.clone() key_column="id".to_string() columns=table_columns.get() - settings= settings.clone() + settings=settings.clone() /> </div> </div> diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 90281cd02..42ca60eee 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -283,7 +283,7 @@ pub fn DefaultConfig() -> impl IntoView { Some(Ok(config)) => { let mut default_config: Vec<Map<String, Value>> = Vec::new(); let settings = TableSettings { - redirect_prefix: None + redirect_prefix: None, }; for (key, value) in config.default_configs.iter() { let mut map = Map::new(); @@ -309,7 +309,7 @@ pub fn DefaultConfig() -> impl IntoView { rows=default_config key_column="id".to_string() columns=table_columns.get() - settings = settings + settings=settings /> </div> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index fb93209f9..4df7a53f0 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -160,77 +160,77 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> imp }; view! { - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill" onclick="my_modal_5.close()"></i> - </button> - </form> - <form - class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - on:submit=on_submit - > - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Dimension</span> - </label> - <input - type="text" - placeholder="Dimension" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=dimension - node_ref=input_element - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Type</span> - </label> - <input - type="text" - placeholder="Type" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=keytype - node_ref=input_element_three - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> - </label> - <input - type="text" - placeholder="Pattern" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=pattern - node_ref=input_element_four - /> - </div> - <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill" onclick="my_modal_5.close()"></i> + </button> + </form> + <form + class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" + on:submit=on_submit > - Submit - </button> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Dimension</span> + </label> + <input + type="text" + placeholder="Dimension" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=dimension + node_ref=input_element + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Type</span> + </label> + <input + type="text" + placeholder="Type" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=keytype + node_ref=input_element_three + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> + </label> + <input + type="text" + placeholder="Pattern" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=pattern + node_ref=input_element_four + /> + </div> + <div class="form-control mt-6"> + <button + type="submit" + class="btn btn-primary shadow-md font-mono" + onclick="my_modal_5.close()" + > + Submit + </button> + </div> + </form> </div> - </form> - </div> - </dialog> + </dialog> } } @@ -294,7 +294,10 @@ pub fn Dimensions() -> impl IntoView { </div> </div> - <ModalComponent handle_submit=Rc::new(move || dimensions.refetch()) tenant=tenant_rs/> + <ModalComponent + handle_submit=Rc::new(move || dimensions.refetch()) + tenant=tenant_rs + /> </div> <div class="card rounded-xl w-full bg-base-100 shadow"> @@ -305,7 +308,7 @@ pub fn Dimensions() -> impl IntoView { {move || { let value = dimensions.get(); let settings = TableSettings { - redirect_prefix: None + redirect_prefix: None, }; match value { Some(v) => { @@ -317,7 +320,7 @@ pub fn Dimensions() -> impl IntoView { view! { <Table table_style="abc".to_string() - rows= data + rows=data key_column="id".to_string() columns=table_columns.get() settings=settings diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index c4054b2cc..be3e14eb6 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,11 +1,25 @@ -use leptos::*; +use chrono::{DateTime, Utc}; +use leptos::{html::Input, logging::log, *}; use leptos_router::use_params_map; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Map, Value}; use tracing::debug; -use chrono::{DateTime, Utc}; +use web_sys::SubmitEvent; + +use crate::{ + api::{fetch_default_config, fetch_dimensions}, + components::{ + experiment_form::experiment_form::ExperimentForm, + table::{ + table::Table, + types::{Column, TableSettings}, + }, + }, +}; -#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display)] +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] #[strum(serialize_all = "UPPERCASE")] pub(crate) enum ExperimentStatusType { CREATED, @@ -39,27 +53,74 @@ pub struct Experiment { pub(crate) traffic_percentage: u8, pub(crate) context: Value, pub(crate) status: ExperimentStatusType, + pub(crate) override_keys: Value, pub(crate) created_by: String, pub(crate) created_at: DateTime<Utc>, pub(crate) last_modified: DateTime<Utc>, - pub(crate) chosen_variant: Option<Variant>, + pub(crate) chosen_variant: Option<String>, } -async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { +async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, String> { let client = reqwest::Client::new(); match client .get(format!("http://localhost:8080/experiments/{}", exp_id)) + .header("x-tenant", tenant) + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} + +async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .patch(format!("http://localhost:8080/experiments/{}/ramp", exp_id)) .header("x-tenant", "mjos") + .json(&json!({ "traffic_percentage": percent })) .send() .await { Ok(experiment) => { debug!("experiment response {:?}", experiment); Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - }, + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} + +async fn conclude_experiment( + exp_id: String, + variant_id: String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + match client + .patch(format!( + "http://localhost:8080/experiments/{}/conclude", + exp_id + )) + .header("x-tenant", "mjos") + .json(&json!({ "chosen_variant": variant_id })) + .send() + .await + { + Ok(experiment) => { + debug!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } Err(e) => Err(e.to_string()), } } @@ -67,138 +128,402 @@ async fn get_experiment(exp_id: &String) -> Result<Experiment, String> { #[component] pub fn experiment_page() -> impl IntoView { let exp_params = use_params_map(); - let experiment_id = move || exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); - let experiment_info = create_resource( - experiment_id, - |exp_id: String| async move { - get_experiment(&exp_id).await - }, - ); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let source = move || { + let t = tenant_rs.get(); + let exp_id = + exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + (exp_id, t) + }; + + let experiment_info = create_resource(source, |(exp_id, tenant)| async move { + get_experiment(&exp_id, &tenant).await + }); view! { - <Transition - fallback= move || view! {<h1> Loading.... </h1>} > - {move || - match experiment_info.get() { - Some(Ok(experiment)) => experiment_detail_view(&experiment).into_view(), - Some(Err(err)) => view! {<h1>{err.to_string()}</h1>}.into_view(), - None => view! {<h1>No elements </h1>}.into_view(), - } - } + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> + {move || match experiment_info.get() { + Some(Ok(experiment)) => { + experiment_detail_view(experiment, experiment_info).into_view() + } + Some(Err(err)) => view! { <h1>{err.to_string()}</h1> }.into_view(), + None => view! { <h1>No elements</h1> }.into_view(), + }} + </Transition> } } -fn experiment_detail_view(exp: &Experiment) -> impl IntoView { +fn experiment_detail_view( + initial_data: Experiment, + exp_resource: Resource<(String, String), Result<Experiment, String>>, +) -> impl IntoView { + let contexts = initial_data.context["and"] + .as_array() + .unwrap_or(&Vec::new()) + .to_owned(); + let (experiment, _) = create_signal(initial_data); + let (ctxs, _) = create_signal(contexts); + view! { <div class="flex flex-col overflow-x-auto p-2"> - <h1 class="text-4xl pt-4 font-extrabold"> - {&exp.name} - <span class="badge ml-3 mb-1 badge-primary badge-lg">{exp.status.to_string()}</span> - </h1> - - <div class="divider"></div> - - <div class="join m-5"> - <button class="btn join-item"><i class="ri-edit-line"></i>Edit</button> - <button class="btn join-item"><i class="ri-stop-circle-line"></i>Conclude</button> - <button class="btn join-item"><i class="ri-guide-line"></i>Release</button> - <button class="btn join-item"><i class="ri-flight-takeoff-line"></i>Ramp</button> - </div> + {move || { + experiment + .with(|exp| { + let class_name = match exp.status { + ExperimentStatusType::CREATED => { + "badge ml-3 mb-1 badge-lg badge-primary" + } + ExperimentStatusType::INPROGRESS => { + "badge ml-3 mb-1 badge-lg badge-warning" + } + ExperimentStatusType::CONCLUDED => { + "badge ml-3 mb-1 badge-lg badge-success" + } + }; + view! { + <h1 class="text-4xl pt-4 font-extrabold"> + {&exp.name} <span class=class_name>{exp.status.to_string()}</span> + </h1> + } + }) + }} + <div class="divider"></div> + <div class="flex flex-row justify-end join m-5"> + {move || { + experiment + .with(|exp| { + match exp.status { + ExperimentStatusType::CREATED => { + view! { + <button + class="btn join-item" + onclick="edit_exp_modal.showModal()" + > + <i class="ri-edit-line"></i> + Edit + </button> + <button + class="btn join-item" + value=&exp.id + on:click=move |button_event| spawn_local(async move { + let value = event_target_value(&button_event); + let _ = ramp_experiment(&value, 1).await; + exp_resource.refetch(); + }) + > - <div class="stats shadow-xl mt-5"> - <div class="stat"> - <div class="stat-title">Experiment ID</div> - <div class="stat-value">{&exp.id}</div> - </div> - <div class="stat"> - <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value">{exp.traffic_percentage}</div> - </div> - <div class="stat"> - <div class="stat-title">Created by</div> - <div class="stat-value">{&exp.created_by}</div> - </div> - <div class="stat"> - <div class="stat-title">Created at</div> - <div class="stat-value">{format!("{}", &exp.created_at.format("%d-%m-%Y %H:%M:%S"))}</div> - </div> - <div class="stat"> - <div class="stat-title">Last Modified</div> - <div class="stat-value">{format!("{}", &exp.last_modified.format("%d-%m-%Y %H:%M:%S"))}</div> - </div> - </div> + <i class="ri-guide-line"></i> + Start + </button> + } + } + ExperimentStatusType::INPROGRESS => { + view! { + <button + class="btn join-item" + onclick="conclude_exp_modal.showModal()" + > + <i class="ri-stop-circle-line"></i> + Conclude + </button> + <button + class="btn join-item" + onclick="ramp_exp_modal.showModal()" + > + <i class="ri-flight-takeoff-line"></i> + Ramp + </button> + } + } + ExperimentStatusType::CONCLUDED => { + view! { + <></> + <div class="stat"> + <div class="stat-title">Chosen Variant</div> + <div class="stat-value"> + {match exp.chosen_variant { + Some(ref v) => format!("{}", v), + None => String::new(), + }} - <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Context</h2> - <div class="flex flex-row"> - <div class="stat"> - <div class="stat-title">Client ID</div> - <div class="stat-value">cac</div> + </div> + </div> + } + } + } + }) + }} + + </div> <div class="stats shadow-xl mt-5"> + <div class="stat"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value">{experiment.get().id}</div> + </div> + <div class="stat"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value">{move || experiment.get().traffic_percentage}</div> + </div> + <div class="stat"> + <div class="stat-title">Created by</div> + <div class="stat-value">{experiment.get().created_by}</div> + </div> + <div class="stat"> + <div class="stat-title">Created at</div> + <div class="stat-value"> + {format!("{}", experiment.get().created_at.format("%v"))} </div> - <div class="divider divider-horizontal">&&</div> - <div class="stat"> - <div class="stat-title">OS</div> - <div class="stat-value">android</div> + </div> + <div class="stat"> + <div class="stat-title">Last Modified</div> + <div class="stat-value"> + {move || { + experiment.with(|exp| format!("{}", &exp.last_modified.format("%v"))) + }} + </div> </div> - </div> - </div> + </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row"> + {move || { + let contexts = move || ctxs.get().into_iter(); + let mut view: Vec<_> = Vec::new(); + for item in contexts() { + for (_, value) in item.as_object().unwrap().into_iter() { + let rule_vector = value.as_array().unwrap(); + let mut rule_iter = rule_vector.to_owned().into_iter(); + let (var, value) = ( + rule_iter.next().unwrap(), + rule_iter.next().unwrap(), + ); + let dimension = var.as_object().unwrap().get("var").unwrap(); + view.push( + view! { + <div class="stat"> + <div class="stat-title"> + {format!("{}", dimension.as_str().unwrap())} + </div> + <div class="stat-value"> + {format!("{}", value.as_str().unwrap())} + </div> + </div> + }, + ) + } + } + view + }} + </div> + </div> + </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto overflow-y-auto"> + {move || { + let exp = move || experiment.get(); + let rows = gen_variant_rows(&exp().variants).unwrap(); + let mut columns: Vec<Column> = Vec::new(); + let settings = TableSettings { + redirect_prefix: None, + }; + columns.push(Column::default("Variant".into())); + for okey in exp().override_keys.as_array().unwrap().into_iter() { + columns.push(Column::default(okey.as_str().unwrap().into())); + } + view! { + <Table + table_style="abc".to_string() + rows=rows + key_column="overrides".to_string() + columns=columns + settings=settings + /> + } + }} - <div class="card bg-base max-w-screen shadow-xl mt-5"> - <div class="card-body"> - <h2 class="card-title">Variants</h2> - <div class="overflow-x-auto"> - <table class="table"> - <thead> - <tr class="bg-base-200"> - <th></th> - <th>Key</th> - <th>Variant-1</th> - <th>Variant-2</th> - <th>Variant-3</th> - <th>Variant-4</th> - <th>Variant-5</th> - <th>Control</th> - </tr> - </thead> - <tbody> - <tr> - <th>1</th> - <td>pmTestKey1</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Quality Control Specialist</td> - <td>Blue</td> - <td>Blue</td> - </tr> - <tr> - <th>2</th> - <td>pmTestKey2</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Desktop Support Technician</td> - <td>Purple</td> - </tr> - <tr> - <th>3</th> - <td>pmTestKey3</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Tax Accountant</td> - <td>Red</td> - </tr> - </tbody> - </table> + </div> </div> </div> </div> - </div> + {add_dialogs(experiment, exp_resource)} + } +} + +fn gen_variant_rows(variants: &Vec<Variant>) -> Result<Vec<Map<String, Value>>, String> { + let mut rows: Vec<Map<String, Value>> = Vec::new(); + for (i, variant) in variants.into_iter().enumerate() { + let variant_name = match variant.variant_type { + VariantType::CONTROL => "Control".into(), + VariantType::EXPERIMENTAL => format!("Variant-{i}"), + }; + let mut m = Map::new(); + m.insert("Variant".into(), variant_name.into()); + m.insert("variant_id".into(), variant.id.clone().into()); + for (o, value) in variant.overrides.as_object().unwrap().into_iter() { + m.insert(o.clone(), value.clone()); + } + rows.push(m); + } + Ok(rows) +} + +fn add_dialogs( + experiment_rs: ReadSignal<Experiment>, + experiment_ws: Resource<(String, String), Result<Experiment, String>>, +) -> impl IntoView { + let input_element: NodeRef<Input> = create_node_ref(); + let experiment = move || experiment_rs.get(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (traffic, set_traffic) = create_signal(experiment().traffic_percentage); + + let on_submit = move |ev: SubmitEvent| { + ev.prevent_default(); + let value = input_element + .get() + .expect("<input> to exist") + .value_as_number() as u8; + spawn_local(async move { + let _ = ramp_experiment(&experiment().id, value).await; + experiment_ws.refetch(); + }); + }; + + let dimensions = create_resource( + move || tenant_rs.get(), + |tenant| async { + match fetch_dimensions(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let default_config = create_resource( + move || tenant_rs.get(), + |tenant| async { + match fetch_default_config(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + match experiment_rs.get().status { + ExperimentStatusType::CREATED => view! { + <dialog id="edit_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Edit Experiment</h3> + <div class="modal-action"> + // <ExperimentForm + // name=experiment_rs.get().name + // context=vec![] + // variants=vec![] + // dimensions=dimensions.get().unwrap_or(vec![]) + // default_config=default_config.get().unwrap_or(vec![]) + // /> + </div> + </div> + </dialog> + } + .into_view(), + ExperimentStatusType::INPROGRESS => view! { + <dialog id="conclude_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Conclude This Experiment</h3> + <p class="py-4"> + Choose a variant to conclude with, this variant becomes + the new default that is served to requests that match this context + </p> + <form method="dialog"> + {move || { + let mut view_arr = vec![]; + for (i, v) in experiment_rs.get().variants.into_iter().enumerate() { + let (variant, _) = create_signal(v); + let view = match variant.get().variant_type { + VariantType::CONTROL => { + view! { + <button + class="btn btn-block btn-outline btn-success m-2" + on:click=move |_| spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone()) + .await + .unwrap(); + experiment_ws.refetch(); + }) + > + + Control + </button> + } + } + VariantType::EXPERIMENTAL => { + view! { + <button + class="btn btn-block btn-outline btn-info m-2" + on:click=move |_| spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone()) + .await + .unwrap(); + experiment_ws.refetch(); + }) + > + + {format!("Variant-{i}")} + </button> + } + } + }; + view_arr.push(view); + } + view_arr + }} + + </form> + <div class="modal-action"> + <form method="dialog"> + <button class="btn">Close</button> + </form> + </div> + </div> + </dialog> + <dialog id="ramp_exp_modal" class="modal"> + <div class="modal-box"> + <h3 class="font-bold text-lg">Ramp up with release</h3> + <p class="py-4">Increase the traffic being redirected to the variants</p> + <form method="dialog" on:submit=on_submit> + <p>{move || traffic.get()}</p> + <input + type="range" + min="0" + max="100" + node_ref=input_element + value=move || experiment_rs.get().traffic_percentage + class="range" + on:input=move |et| { + let t = event_target_value(&et).parse::<u8>().unwrap(); + log!("traffic value:{t}"); + set_traffic.set(t); + } + /> + + <button class="btn btn-block btn-outline btn-success m-2">Set</button> + </form> + <div class="modal-action"> + <form method="dialog"> + <button class="btn">Close</button> + </form> + </div> + </div> + </dialog> + }.into_view(), + ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), } } diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index eaa6018ad..6bc1df05f 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -1,8 +1,388 @@ use leptos::*; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use wasm_bindgen::JsCast; +use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; + +use crate::{api::fetch_dimensions, components::context_form::context_form::ContextForm}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} + +pub async fn fetch_config(tenant: String) -> Result<Config, String> { + let client = reqwest::Client::new(); + let url = "http://localhost:8080/config"; + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { + let client = reqwest::Client::new(); + let url = format!("http://localhost:8080/config/resolve?{context}"); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} #[component] -pub fn Home() -> impl IntoView { +pub fn home() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let config_data = create_blocking_resource( + move || tenant_rs.get(), + move |tenant| fetch_config(tenant), + ); + let dimension_resource = create_resource( + move || tenant_rs.get(), + |tenant| async { + match fetch_dimensions(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let gen_name_id = |s1: &String, s2: &String| format!("{s1}::{s2}"); + + let gen_query_context = |query: Vec<(String, String, String)>| -> String { + let mut context: Vec<String> = vec![]; + for (dimension, op, value) in query.iter() { + let op = match op.as_str() { + "==" => "=", + _ => break, // query params do not support the other operators : != and IN, do something differently later + }; + context.push(format!("{}{op}{}", dimension.to_lowercase(), value.to_lowercase())); + } + context.join("&").to_string() + }; + + let resolve_click = move |ev: MouseEvent| { + ev.prevent_default(); + let dimension_labels = document().get_elements_by_name("context-dimension-name"); + let dimension_ops = document().get_elements_by_name("context-dimension-operator"); + let dimension_values = document().get_elements_by_name("context-dimension-value"); + let mut query_vector: Vec<(String, String, String)> = vec![]; + for i in 0..dimension_labels.length() { + query_vector.push(( + dimension_labels + .item(i) + .expect("missing input") + .dyn_ref::<HtmlSpanElement>() + .unwrap() + .inner_text(), + dimension_ops + .item(i) + .expect("missing input") + .dyn_ref::<HtmlSelectElement>() + .unwrap() + .value(), + dimension_values + .item(i) + .expect("missing input") + .dyn_ref::<HtmlInputElement>() + .unwrap() + .value(), + )) + } + // strike out all config elements on the page + let config_name_elements = document().get_elements_by_class_name("config-name"); + let config_value_elements = document().get_elements_by_class_name("config-value"); + for i in 0..config_name_elements.length() { + let (config_name_element, config_value_element) = ( + config_name_elements.item(i).unwrap(), + config_value_elements.item(i).unwrap(), + ); + config_name_element.set_inner_html( + format!( + "<s>{}</s>", + config_name_element + .inner_html() + .replace("<s>", "") + .replace("</s>", "") + ) + .as_str(), + ); + config_value_element.set_inner_html( + format!( + "<s>{}</s>", + config_value_element + .inner_html() + .replace("<s>", "") + .replace("</s>", "") + ) + .as_str(), + ); + } + logging::log!("query vector {:#?}", query_vector); + // resolve the context and get the config that would apply + spawn_local(async move { + let context = gen_query_context(query_vector); + let config = match resolve_config(tenant_rs.get(), context).await.unwrap() { + Value::Object(m) => m, + _ => Map::new(), + }; + // unstrike those that we want to show the user + for (dimension, value) in config.into_iter() { + let search_field: String = + gen_name_id(&dimension, &value.as_str().unwrap().to_string()); + logging::log!("gen ID search param {}", search_field); + let config_name_elements = document() + .get_elements_by_name(format!("{}-1", &search_field).as_str()); + let config_value_elements = document() + .get_elements_by_name(format!("{}-2", &search_field).as_str()); + for i in 0..config_name_elements.length() { + let item_one = config_name_elements.item(i).expect("missing span"); + let item_two = config_value_elements.item(i).expect("missing span"); + + let (config_name_element, config_value_element) = ( + item_one.dyn_ref::<HtmlSpanElement>().unwrap(), + item_two.dyn_ref::<HtmlSpanElement>().unwrap(), + ); + let (name, value) = ( + config_name_element + .inner_html() + .replace("<s>", "") + .replace("</s>", ""), + config_value_element + .inner_html() + .replace("<s>", "") + .replace("</s>", ""), + ); + + logging::log!("config name after replace {} and value {}", name, value); + config_name_element.set_inner_html(format!("{}", name).as_str()); + config_value_element.set_inner_html(format!("{}", value).as_str()); + } + } + }); + }; view! { - <h1> Welcome to Context Aware Config </h1> + <div class="flex w-full flex-row mt-5 justify-evenly"> + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + {move || { + dimension_resource + .with(|dimension| { + view! { + <div class="card m-10 bg-base w-4/12"> + <div class="card-body"> + <h2 class="card-title">Resolve Configs</h2> + + <ContextForm + dimensions=dimension.to_owned().unwrap_or(vec![]) + context=vec![] + /> + <div class="card-actions justify-end"> + <button class="btn btn-primary" on:click=resolve_click> + Resolve + </button> + </div> + </div> + </div> + } + }) + }} + + </Suspense> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let rows = |k: &String, v: &Value| { + let key = k.replace("\"", "").trim().to_string(); + let value = format!("{}", v) + .replace("\"", "") + .trim() + .to_string(); + let name_identifier = gen_name_id(&key, &value); + view! { + <tr> + <td> + <span name={format!("{}-1", &name_identifier)} class="config-name"> + {key} + </span> + </td> + <td> + <span name={format!("{}-2", &name_identifier)} class="config-value"> + {value} + </span> + </td> + </tr> + } + }; + let contexts_views: Vec<_> = config + .contexts + .iter() + .map(|context| { + let condition = extract_and_format(&context.condition); + let rows: Vec<_> = context + .override_with_keys + .iter() + .filter_map(|key| config.overrides.get(key)) + .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .map(|(k, v)| { rows(&k, &v) }) + .collect(); + view! { + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title"> + "Condition: " + <div class="badge badge-lg badge-primary p-5"> + {&condition} + </div> + </h2> + <table class="table mt-10"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{rows}</tbody> + </table> + + </div> + </div> + } + }) + .collect::<Vec<_>>(); + let new_context_views = contexts_views + .into_iter() + .rev() + .collect::<Vec<_>>(); + let default_config: Vec<_> = config + .default_configs + .iter() + .map(|(k, v)| { rows(&k, &v) }) + .collect(); + vec![ + view! { + <div class="mb-4 w-8/12 overflow-y-auto max-h-screen"> + {new_context_views} + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title">Default Configuration</h2> + <table class="table"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{default_config}</tbody> + </table> + </div> + </div> + </div> + }, + ] + } + Some(Err(error)) => { + vec![ + view! { + <div class="error"> + {"Failed to fetch config data: "} {error} + </div> + }, + ] + } + None => { + vec![view! { <div class="error">{"No config data fetched"}</div> }] + } + } + })} + + </Suspense> + </div> } } diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs index d4e7591eb..f65456641 100644 --- a/crates/frontend/src/pages/NotFound/NotFound.rs +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -16,7 +16,5 @@ pub fn NotFound() -> impl IntoView { resp.set_status(actix_web::http::StatusCode::NOT_FOUND); } - view! { - <h1>"Not Found"</h1> - } + view! { <h1>"Not Found"</h1> } } From 6376194a495670c5672765d0ee4e494e1c62e764 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Tue, 12 Dec 2023 22:09:37 +0530 Subject: [PATCH 210/352] feat: fixed theme + ui changes + form validation + context validation error handling --- .../src/api/context/handlers.rs | 26 +++- crates/frontend/src/app.rs | 2 +- .../src/components/Button/EditButton.rs | 11 ++ crates/frontend/src/components/Button/mod.rs | 1 + crates/frontend/src/components/mod.rs | 1 + .../src/components/nav_item/nav_item.rs | 5 +- .../src/components/side_nav/side_nav.rs | 2 +- .../pages/ContextOverride/ContextOverride.rs | 116 ++++++++++----- .../src/pages/DefaultConfig/DefaultConfig.rs | 8 +- .../src/pages/Dimensions/Dimensions.rs | 138 ++++++++++-------- crates/frontend/src/pages/Experiment/mod.rs | 2 +- crates/frontend/styles/tailwind.css | 4 + 12 files changed, 201 insertions(+), 115 deletions(-) create mode 100644 crates/frontend/src/components/Button/EditButton.rs create mode 100644 crates/frontend/src/components/Button/mod.rs diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 74018cec2..1407e1353 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -17,7 +17,7 @@ use crate::{ }; use actix_web::{ delete, - error::{self, ErrorInternalServerError, ErrorNotFound}, + error::{self, ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, get, put, web::{self, Path}, HttpResponse, Responder, Result, Scope, @@ -272,9 +272,27 @@ async fn put_handler( ) -> actix_web::Result<web::Json<PutResp>> { put(req, &user, &mut db_conn, false) .map(|resp| web::Json(resp)) - .map_err(|e| { + .map_err(|e: anyhow::Error| { log::info!("context put failed with error: {:?}", e); - ErrorInternalServerError("") + if let Some(io_error) = e.downcast_ref::<std::io::Error>() { + log::info!("{}", { io_error }); + ErrorInternalServerError("") + } else if e.to_string().contains("Bad schema") { + ErrorBadRequest("") + } else { + ErrorInternalServerError("") + } + //TODO: Check why this is not working + // match e.downcast_ref::<std::io::Error>() { + // Some(err) => { + // if err.to_string().contains("Bad schema") { + // ErrorBadRequest("Schema Validation Failed") + // } else { + // ErrorInternalServerError("") + // } + // } + // None => ErrorInternalServerError(""), + // } }) } @@ -535,4 +553,4 @@ async fn bulk_operations( Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} +} \ No newline at end of file diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index d1edcb1ef..fc087d131 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -29,7 +29,7 @@ pub fn App() -> impl IntoView { <Title text="Welcome to Context Aware Config"/> // content for this welcome page <Router> - <body class="m-0 min-h-screen bg-gray-50"> + <body class="m-0 min-h-screen bg-gray-50 font-mono"> <Layout> <Routes> <Route diff --git a/crates/frontend/src/components/Button/EditButton.rs b/crates/frontend/src/components/Button/EditButton.rs new file mode 100644 index 000000000..a66a2f2fd --- /dev/null +++ b/crates/frontend/src/components/Button/EditButton.rs @@ -0,0 +1,11 @@ +use leptos::*; + +#[component] +pub fn EditButton(text: String, modal: String, modalAction: String) -> impl IntoView { + view! { + <button class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" onclick=format!("{modal}.{modalAction}")> + {text} + <i class="ri-edit-2-line ml-2"></i> + </button> + } +} diff --git a/crates/frontend/src/components/Button/mod.rs b/crates/frontend/src/components/Button/mod.rs new file mode 100644 index 000000000..e52febfcf --- /dev/null +++ b/crates/frontend/src/components/Button/mod.rs @@ -0,0 +1 @@ +pub mod EditButton; diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 674effa64..1dbda68e6 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,3 +1,4 @@ +pub mod Button; pub mod context_form; pub mod experiment_form; pub mod nav_item; diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 4ca872413..1525fdc7e 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -11,14 +11,15 @@ pub fn NavItem( let (anchor_class, icon_wrapper_class, icon_class) = if is_active { ( "py-2.5 px-4 flex items-center whitespace-nowrap active".to_string(), - "rounded-lg bg-primary w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string(), + "text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none + focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 w-8 h-8 flex content-center justify-center pt-0.5 px-1 ".to_string(), format!("{} text-lg text-white font-normal", icon) ) } else { ( "py-2.5 px-4 flex items-center whitespace-nowrap".to_string(), "rounded-lg shadow-xl shadow-slate-300 bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string(), - format!("{} text-lg text-primary font-normal", icon) + format!("{} text-lg text-purple-800 font-normal", icon) ) }; diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 4304bdcee..be5d95845 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -59,7 +59,7 @@ pub fn SideNav() -> impl IntoView { ); } - class="select w-full max-w-xs" + class="select w-full max-w-xs shadow-md" > <option selected>mjos</option> <option>sdk_config</option> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 88854acfc..1ff0dd395 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,15 +1,17 @@ // use std::collections::HashMap; use std::rc::Rc; -use crate::api::{fetch_dimensions, fetch_default_config}; +use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; +use crate::components::Button::EditButton::EditButton; use crate::pages::DefaultConfig::types::Config; // use leptos::spawn_local; use leptos::*; use leptos_router::use_query_map; +use reqwest::{Error, StatusCode}; use serde_json::{json, Map, Value}; use web_sys::SubmitEvent; @@ -26,10 +28,6 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { } } - - - - #[component] fn ContextModalForm() -> impl IntoView { let query = use_query_map(); @@ -81,8 +79,6 @@ fn ContextModalForm() -> impl IntoView { } } - - pub fn construct_request_payload( overrides: Map<String, Value>, conditions: Vec<(String, String, String)>, @@ -143,7 +139,11 @@ pub async fn create_context( .send() .await .map_err(|e| e.to_string())?; - response.text().await.map_err(|e| e.to_string()) + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } } #[component] @@ -289,6 +289,8 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let (ovrride_form, set_ovrride_form) = create_signal(("dummy".to_string(), Map::new())); + let (error_message, set_error_message) = create_signal("".to_string()); + let on_submit = { move |ev: SubmitEvent| { let handle_submit_clone = handle_submit.clone(); @@ -332,12 +334,22 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { .await; match result { - Ok(_) => { + Ok(str) => { handle_submit(); + // log!("Hi babe{str}"); + + js_sys::eval( + "document.getElementById('my_modal_5').close();", + ) + .unwrap(); } - Err(_) => { - // Handle error - // We can consider logging or displaying the error + Err(e) => { + if e.is_empty() { + set_error_message + .set("Internal_Server_error".to_string()); + } else { + set_error_message.set(e); + } } } } @@ -347,11 +359,8 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { <div class="p-6 text-gray-600 space-y-6"> - <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> - Create Context Overrides - <i class="ri-edit-2-line ml-2"></i> - </button> - // + <EditButton text="Create Context Overrides".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> + // <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> <form method="dialog" class="flex justify-end"> @@ -370,11 +379,19 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <button type="submit" class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" + // onclick="my_modal_5.close()" > Submit </button> </div> + { + + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } </form> </div> @@ -385,7 +402,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { fn parse_conditions(input: String) -> Vec<(String, String, String)> { let mut conditions = Vec::new(); - let operators = vec!["==", "in", "!="]; // Define your operators here + let operators = vec!["==", "in"]; // Split the string by "and" and iterate over each condition for condition in input.split("and") { @@ -396,17 +413,33 @@ fn parse_conditions(input: String) -> Vec<(String, String, String)> { for operator in &operators { if condition.contains(operator) { operator_found = operator; - parts = condition.split(operator).collect(); + parts = condition.split(operator).map(|s| s.trim()).collect(); + + // TODO: add this when context update is enabled + if parts.len() == 2 && operator == &"in" { + parts.swap(0, 1); + } + break; } } if parts.len() == 2 { - conditions.push(( - parts[0].trim().to_string(), - operator_found.to_string(), - parts[1].trim().to_string(), - )); + let mut key = parts[0].to_string(); + let mut op = operator_found.to_string(); + let mut val = parts[1].to_string(); + // Add a space after key + key.push(' '); + if op == "==".to_string() { + val = val.trim_matches('"').to_string(); + op = "is".to_string(); + } else { + val = val.trim_matches('"').to_string(); + op = "has".to_string(); + } + op.push(' '); + + conditions.push((key, op, val)); } } @@ -463,12 +496,12 @@ pub fn ContextOverride() -> impl IntoView { redirect_prefix: None, }; let mut context_views = Vec::new(); - let mut new_ctx: Vec<(String, String, String)> = vec![]; + // let mut new_ctx: Vec<(String, String, String)> = vec![]; let mut override_signal = Map::new(); for context in config.contexts.iter() { let condition = extract_and_format(&context.condition); let ctx_values = parse_conditions(condition.clone()); - new_ctx.extend(ctx_values); + // new_ctx.extend(ctx_values.clone()); for key in context.override_with_keys.iter() { let mut map = Map::new(); let ovr = config.overrides.get(key).unwrap(); @@ -490,20 +523,27 @@ pub fn ContextOverride() -> impl IntoView { context_views .push( view! { - <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6"> - <div class="card-body bg-white dark:bg-gray-800 bg-white shadow-md"> + <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6 shadow-md"> + <div class="flex justify-between"> <div class="flex items-center space-x-4"> - <i class="ri-settings-5-line ri-xl text-blue-500"></i> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> + + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white shadow-md font-mono"> "Condition" </h2> <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> - <div class="badge badge-primary font-mono">{condition}</div> - </div> - <div class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> - <i class="ri-edit-line text-blue-500"></i> + {ctx_values.into_iter().map(|(dim,op,val)| view!{ + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> + <span class="font-mono font-medium context_condition text-gray-500">{dim}</span> + <span class="font-mono font-medium text-gray-650 context_condition ">{op}</span> + <span class="font-mono font-semibold context_condition">{val}</span> + </span> + + }).collect::<Vec<_>>()} </div> + <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> + <i class="ri-edit-line text-blue-500"></i> + </button> </div> <div class="space-x-4"> <Table @@ -514,14 +554,14 @@ pub fn ContextOverride() -> impl IntoView { settings=settings.clone() /> </div> - </div> + </div> }, ); contexts.clear(); } - ctx.set(new_ctx); + // ctx.set(new_ctx); ovr_data.set(override_signal); context_views } @@ -543,4 +583,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 42ca60eee..64ca4d54f 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -5,6 +5,7 @@ use crate::components::table::{ table::Table, types::{Column, TableSettings}, }; +use crate::components::Button::EditButton::EditButton; use crate::pages::DefaultConfig::types::Config; use js_sys; use leptos::ev::SubmitEvent; @@ -63,10 +64,7 @@ pub async fn create_default_config( fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { <div class="p-6 bg-white text-gray-600"> - <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> - Create DefaultConfig - <i class="ri-edit-2-line ml-2"></i> - </button> + <EditButton text="Create DefaultConfig".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> <div class="modal-box relative bg-white"> <form method="dialog" class="flex justify-end"> @@ -301,7 +299,7 @@ pub fn DefaultConfig() -> impl IntoView { view! { <div class="card rounded-lg w-full bg-base-100 shadow"> <div class="card-body"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white font-mono"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> "Default Config" </h2> <Table diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 4df7a53f0..68b45cc8d 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::rc::Rc; +use crate::components::Button::EditButton::EditButton; use leptos::logging::*; use leptos::*; use serde_json::{json, Map, Value}; @@ -15,84 +16,96 @@ pub struct RowData { pub dimension: String, pub priority: String, pub type_: String, - pub pattern: String + pub pattern: String, } pub fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { - let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); - let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); - let row_priority = row["priority"].clone().to_string().replace("\"", ""); - - let schema = row["schema"].clone().to_string(); - let schema_object = serde_json::from_str::<serde_json::Value>(&schema).unwrap(); - - let row_type = schema_object.get("type").unwrap().to_string(); - let row_pattern = schema_object.get("pattern").unwrap().to_string(); - - let edit_click_handler = move |_| { - let row_data = RowData { - dimension: row_dimension.clone(), - priority: row_priority.clone(), - type_: row_type.clone(), - pattern: row_pattern.clone(), - }; - intermediate_signal.set(Some(row_data)); - js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); - }; - - let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; - - view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() + let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); + let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); + let row_priority = row["priority"].clone().to_string().replace("\"", ""); + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<serde_json::Value>(&schema).unwrap(); + + let row_type = schema_object.get("type").unwrap().to_string(); + let row_pattern = schema_object + .get("pattern") + .unwrap_or(&Value::String("".to_string())) + .to_string(); + + let edit_click_handler = move |_| { + let row_data = RowData { + dimension: row_dimension.clone(), + priority: row_priority.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + }; + intermediate_signal.set(Some(row_data)); + js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); + }; + + let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + + view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() } pub async fn create_dimension( - tenant: String, - key: String, - priority: String, - key_type: String, - pattern: String, + tenant: String, + key: String, + priority: String, + key_type: String, + pattern: String, ) -> Result<String, String> { - let priority: i64 = priority.parse().unwrap(); - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/dimension"); - - let mut req_body: HashMap<&str, Value> = HashMap::new(); - let mut schema: Map<String, Value> = Map::new(); - - schema.insert("type".to_string(), Value::String(key_type.replace("\"", ""))); - schema.insert("pattern".to_string(), Value::String(pattern.replace("\"", ""))); - - req_body.insert("dimension", Value::String(key)); - req_body.insert("priority", Value::Number(priority.into())); - req_body.insert("schema", Value::Object(schema)); - - let response = client - .put(url) - .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") - .json(&req_body) - .send() - .await - .map_err(|e| e.to_string())?; - response.text().await.map_err(|e| e.to_string()) + let priority: i64 = priority.parse().unwrap(); + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/dimension"); + + let mut req_body: HashMap<&str, Value> = HashMap::new(); + let mut schema: Map<String, Value> = Map::new(); + + schema.insert( + "type".to_string(), + Value::String(key_type.replace("\"", "")), + ); + schema.insert( + "pattern".to_string(), + Value::String(pattern.replace("\"", "")), + ); + + req_body.insert("dimension", Value::String(key)); + req_body.insert("priority", Value::Number(priority.into())); + req_body.insert("schema", Value::Object(schema)); + + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&req_body) + .send() + .await + .map_err(|e| e.to_string())?; + response.text().await.map_err(|e| e.to_string()) } #[component] -fn ModalComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> impl IntoView { +fn ModalComponent( + handle_submit: Rc<dyn Fn()>, + tenant: ReadSignal<String>, +) -> impl IntoView { view! { <div class="pt-4"> - <button class="btn btn-outline btn-primary" onclick="my_modal_5.showModal()"> - Create Dimension - <i class="ri-edit-2-line ml-2"></i> - </button> + <EditButton text="Create Dimension".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> <FormComponent handle_submit=handle_submit tenant=tenant/> </div> } } #[component] -fn FormComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> impl IntoView { +fn FormComponent( + handle_submit: Rc<dyn Fn()>, + tenant: ReadSignal<String>, +) -> impl IntoView { use leptos::html::Input; let handle_submit = handle_submit.clone(); let global_state = use_context::<RwSignal<RowData>>(); @@ -234,11 +247,10 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>, tenant: ReadSignal<String>) -> imp } } - #[component] pub fn Dimensions() -> impl IntoView { - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let global_state = create_rw_signal(RowData::default()); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let global_state = create_rw_signal(RowData::default()); provide_context(global_state); let intermediate_signal = create_rw_signal(None::<RowData>); diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index be3e14eb6..f95f368b8 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -69,7 +69,7 @@ async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, .await { Ok(experiment) => { - debug!("experiment response {:?}", experiment); + // debug!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index 683974950..c02cf642c 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -11,4 +11,8 @@ @apply text-black; @apply shadow-lg; @apply font-bold; +} + +.context_condition{ + font-size: 0.9rem; } \ No newline at end of file From 588d15a9dc9dd59b870ac473a052dfd911239ab3 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 13 Dec 2023 12:14:21 +0530 Subject: [PATCH 211/352] feat: experiment create form --- .../components/context_form/context_form.rs | 332 +++++++++--------- .../src/components/context_form/mod.rs | 1 + .../src/components/context_form/utils.rs | 77 ++++ .../experiment_form/experiment_form.rs | 233 +++++++----- .../src/components/experiment_form/mod.rs | 2 + .../src/components/experiment_form/types.rs | 13 + .../src/components/experiment_form/utils.rs | 36 ++ crates/frontend/src/components/mod.rs | 1 + .../src/components/nav_item/nav_item.rs | 4 +- .../components/override_form/override_form.rs | 247 +++++++------ crates/frontend/src/components/stat/mod.rs | 1 + crates/frontend/src/components/stat/stat.rs | 21 ++ .../pages/ContextOverride/ContextOverride.rs | 262 +++++++------- .../src/pages/DefaultConfig/DefaultConfig.rs | 159 ++++----- .../src/pages/Dimensions/Dimensions.rs | 52 +-- crates/frontend/src/pages/Experiment/mod.rs | 4 +- .../pages/ExperimentList/ExperimentList.rs | 129 +++++-- .../src/pages/ExperimentList/types.rs | 4 +- scripts/create-tenant.sh | 2 +- 19 files changed, 933 insertions(+), 647 deletions(-) create mode 100644 crates/frontend/src/components/context_form/utils.rs create mode 100644 crates/frontend/src/components/experiment_form/types.rs create mode 100644 crates/frontend/src/components/experiment_form/utils.rs create mode 100644 crates/frontend/src/components/stat/mod.rs create mode 100644 crates/frontend/src/components/stat/stat.rs diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index b1738ec84..0b18c3b31 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -3,201 +3,213 @@ use leptos::*; use std::cmp; use std::collections::HashSet; use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, HtmlSelectElement}; +use web_sys::{HtmlInputElement, HtmlSelectElement, SubmitEvent, MouseEvent}; use serde_json::json; #[component] -pub fn ContextForm( +pub fn ContextForm<NF>( + handle_change: NF, dimensions: Vec<Dimension>, + is_standalone: bool, context: Vec<(String, String, String)>, ) -> impl IntoView +where + NF: Fn(Vec<(String, String, String)>) + 'static { - let (context, set_context) = create_signal(context); - let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); + let has_dimensions = dimensions.len() > 0; + let (default_condition, default_used_dimension) = if context.len() == 0 && has_dimensions { + let dimension = dimensions[0].dimension.to_string(); + ( + vec![(dimension.to_string(), "".to_string(), "".to_string())], + vec![dimension.to_string()] + ) + } else { + ( + context.clone(), + context.into_iter().map(|item| item.0.to_string()).collect::<Vec<String>>() + ) + }; + + let (context, set_context) = create_signal(default_condition); + let (used_dimensions, set_used_dimensions) = create_signal(HashSet::from_iter(default_used_dimension)); let total_dimensions = dimensions.len(); - let condition_context = - use_context::<RwSignal<Vec<(String, String, String, String)>>>(); + let last_idx = create_memo(move |_| { let len = context.get().len(); cmp::max(0, len - 1) }); + let on_click = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Context form submit"); + //TODO: submit logic for this + }; + create_effect(move |_| { - let values = context - .get() - .clone() - .into_iter() - .enumerate() - .map(|(idx, (dimension, operator, value))| { - (idx.to_string(), dimension.to_string(), operator.to_string(), value.to_string()) - }) - .collect::<Vec<(String, String, String, String)>>(); - if let Some(c_context) = condition_context { - c_context.set(values.clone()); - } + let f_context = context.get(); + handle_change(f_context.clone()); }); view! { - <div class="form-control w-full "> - <div class="flex gap-4 justify-between"> - <label class="label"> - <span class="label-text font-semibold text-base">Context</span> - </label> - <div> - <div class="dropdown dropdown-left"> - <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> - <i class="ri-add-line"></i> - Add Dimension - </label> - <ul - tabindex="0" - class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" - > - <For - each=move || { - dimensions - .clone() - .into_iter() - .filter(|dim| { - !used_dimensions.get().contains(&dim.dimension) - }) - .collect::<Vec<Dimension>>() - } + <div> + <div class="form-control w-full "> + <div class="flex gap-4 justify-between"> + <label class="label"> + <span class="label-text font-semibold text-base">Context</span> + </label> + <div> + <div class="dropdown dropdown-left"> + <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> + <i class="ri-add-line"></i> + Add Dimension + </label> + <ul + tabindex="0" + class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" + > + <For + each=move || { + dimensions + .clone() + .into_iter() + .filter(|dim| { + !used_dimensions.get().contains(&dim.dimension) + }) + .collect::<Vec<Dimension>>() + } - key=|dimension: &Dimension| dimension.dimension.to_string() - children=move |dimension: Dimension| { - let dimension_name = dimension.dimension.to_string(); - let label = dimension_name.to_string(); - view! { - <li on:click=move |_| { - set_context - .update(|value| { - leptos::logging::log!("{:?}", value); - value - .push(( - dimension_name.to_string(), - "".to_string(), - "".to_string(), - )) - }); - set_used_dimensions - .update(|value| { - value.insert(dimension_name.to_string()); - }); - }> + key=|dimension: &Dimension| dimension.dimension.to_string() + children=move |dimension: Dimension| { + let dimension_name = dimension.dimension.to_string(); + let label = dimension_name.to_string(); + view! { + <li on:click=move |_| { + set_context + .update(|value| { + leptos::logging::log!("{:?}", value); + value + .push(( + dimension_name.to_string(), + "".to_string(), + "".to_string(), + )) + }); + set_used_dimensions + .update(|value: &mut HashSet<String>| { + value.insert(dimension_name.to_string()); + }); + }> - <a>{label.to_string()}</a> - </li> + <a>{label.to_string()}</a> + </li> + } } - } - /> + /> - </ul> + </ul> + </div> </div> </div> - </div> - <div class="p-4"> - <For - each=move || { - context - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, (String, String, String))>>() - } - - key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) - children=move |(idx, (dimension, operator, value))| { - let dimension_label = dimension.to_string(); - let dimension_name = dimension.to_string(); - view! { - <div class="flex gap-x-6"> - <div class="form-control w-20"> - <label class="label font-medium font-mono text-sm"> - <span class="label-text">Operator</span> - </label> - <select - bind:value=operator - on:input=move |event| { - let input_value = event_target_value(&event); - set_context - .update(|curr_context| { - curr_context[idx].1 = input_value; - }); - } - - name="context-dimension-operator" - class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" - > - <option disabled selected> - Pick one - </option> - <option value="==">==</option> - <option value="!=">!=</option> - <option value="IN">IN</option> - </select> - - </div> - <div class="form-control"> - <label class="label capitalize font-mono text-sm"> - <span name="context-dimension-name" class="label-text"> - {dimension_label} - </span> - </label> - <div class="flex gap-x-6 items-center"> - <input - bind:value=value + <div class="p-4"> + <For + each=move || { + context + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, String, String))>>() + } + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) + children=move |(idx, (dimension, operator, value))| { + let dimension_label = dimension.to_string(); + let dimension_name = dimension.to_string(); + view! { + <div class="flex gap-x-6"> + <div class="form-control w-20"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">Operator</span> + </label> + <select + bind:value=operator on:input=move |event| { let input_value = event_target_value(&event); - set_context - .update(|curr_context| { - curr_context[idx].2 = input_value; - }); - } - - name="context-dimension-value" - type="text" - placeholder="Type here" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - /> - <button - class="btn btn-ghost btn-circle btn-sm" - on:click=move |_| { - set_context - .update(|value| { - value.remove(idx); - }); - set_used_dimensions - .update(|value| { - value.remove(&dimension_name); - }); + set_context.update(|curr_context| { + // setting operator + curr_context[idx].1 = input_value; + }); } + class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" > + <option disabled selected> + Pick one + </option> + <option value="==">==</option> + <option value="!=">!=</option> + <option value="IN">IN</option> + </select> - <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> - </button> + </div> + <div class="form-control"> + <label class="label capitalize font-mono text-sm"> + <span class="label-text">{dimension_label}</span> + </label> + <div class="flex gap-x-6 items-center"> + <input + bind:value=value + on:input=move |event| { + let input_value = event_target_value(&event); + set_context.update(|curr_context| { + curr_context[idx].2 = input_value; + }); + } + type="text" + placeholder="Type here" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + /> + <button + class="btn btn-ghost btn-circle btn-sm" + on:click=move |_| { + set_context + .update(|value| { + value.remove(idx); + }); + set_used_dimensions + .update(|value| { + value.remove(&dimension_name); + }); + } + > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> + </div> </div> </div> - </div> - {move || { - if last_idx.get() != idx { - view! { - <div class="my-3 ml-5 ml-6 ml-7"> - <span class="font-mono text-xs">"&&"</span> - </div> + {move || { + if last_idx.get() != idx { + view! { + <div class="my-3 ml-5 ml-6 ml-7"> + <span class="font-mono text-xs">"&&"</span> + </div> + } + .into_view() + } else { + view! {}.into_view() } - .into_view() - } else { - view! {}.into_view() - } - }} + }} + } } - } - /> - + /> + </div> </div> + <Show + when=move || is_standalone + > + <div class="flex justify-end"> + <button class="btn" on:click:undelegated=on_click>Save</button> + </div> + </Show> </div> } } \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/mod.rs b/crates/frontend/src/components/context_form/mod.rs index 73287c04b..d7910011f 100644 --- a/crates/frontend/src/components/context_form/mod.rs +++ b/crates/frontend/src/components/context_form/mod.rs @@ -1 +1,2 @@ pub mod context_form; +pub mod utils; \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs new file mode 100644 index 000000000..4228c54e4 --- /dev/null +++ b/crates/frontend/src/components/context_form/utils.rs @@ -0,0 +1,77 @@ +use serde_json::{json, Value, Map}; +use reqwest::{Error, StatusCode}; + +pub fn construct_context( + conditions: Vec<(String, String, String)>, +) -> Value { + let context = if conditions.len() == 1 { + // Single condition + let (variable, operator, value) = &conditions[0]; + json!({ + operator: [ + { "var": variable }, + value + ] + }) + } else { + // Multiple conditions inside an "and" + let and_conditions: Vec<Value> = conditions + .into_iter() + .map(|(variable, operator, value)| { + json!({ + operator: [ + { "var": variable }, + value + ] + }) + }) + .collect(); + + json!({ "and": and_conditions }) + }; + + context +} + +pub fn construct_request_payload( + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, +) -> Value { + // Construct the override section + let override_section: Map<String, Value> = overrides; + + // Construct the context section + let context_section = construct_context(conditions); + + // Construct the entire request payload + let request_payload = json!({ + "override": override_section, + "context": context_section + }); + + request_payload +} + +pub async fn create_context( + tenant: String, + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/context"); + let request_payload = construct_request_payload(overrides, conditions); + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 1f556253b..25aae9518 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -4,41 +4,76 @@ use crate::components::{ use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; +use super::utils::create_experiment; use leptos::*; -use serde_json::Map; +use serde_json::{Value, Map}; use wasm_bindgen::JsCast; -use web_sys::{SubmitEvent, HtmlInputElement}; +use web_sys::{SubmitEvent, HtmlInputElement, MouseEvent}; +use std::sync::RwLock; #[component] -pub fn ExperimentForm( +pub fn ExperimentForm<NF>( name: String, context: Vec<(String, String, String)>, variants: Vec<Variant>, dimensions: Vec<Dimension>, default_config: Vec<DefaultConfig>, -) -> impl IntoView { - // let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + handle_submit: NF +) -> impl IntoView +where + NF: Fn() + 'static + Clone +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (experiment_name, set_experiment_name) = create_signal(name); - let (variants, set_variants) = create_signal(variants); - // let input_vector: InputVector = vec![(experiment_name, set_experiment_name)]; - let on_submit = move |ev: SubmitEvent| { - ev.prevent_default(); - let document = document(); - let exp_name = document - .get_element_by_id("expName") - .expect("expName input not found"); - logging::log!("{:#?}", exp_name.get_attribute_names()); - logging::log!("{:#?}", exp_name.get_attribute("value").unwrap()); - let variant_ids = document.get_elements_by_name("variantId"); - logging::log!("{:#?}", variant_ids.length()); - logging::log!("{:#?}", variant_ids.item(0).expect("missing input").dyn_ref::<HtmlInputElement>().unwrap().value()); - let override_vec = document.get_elements_by_name("override"); - logging::log!("{:#?}", override_vec.length()); - logging::log!("{:#?}", override_vec.item(0).expect("missing override input").dyn_ref::<HtmlInputElement>().unwrap().value()); + let (f_context, set_context) = create_signal(context.clone()); + let (f_variants, set_variants) = create_signal(variants); + + let handle_context_form_change = move |updated_ctx: Vec<(String, String, String)>| { + set_context.set(updated_ctx); + }; + + let handle_override_form_change = move |variant_idx: usize| { + let handle_change = move |updated_overrides: Map<String, Value>| { + set_variants.update(|curr_variants| { + curr_variants[variant_idx].overrides = updated_overrides.clone(); + }); + }; + handle_change + }; + + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting experiment form"); + + let f_experiment_name = experiment_name.get(); + let f_context = f_context.get(); + let f_variants = f_variants.get(); + let tenant = tenant_rs.get(); + let handle_submit_clone = handle_submit.clone(); + + logging::log!("{:?}", f_experiment_name); + logging::log!("{:?}", f_variants); + logging::log!("{:?}", f_context); + + spawn_local({ + async move { + let result = create_experiment(f_context, f_variants, f_experiment_name, tenant).await; + + match result { + Ok(value) => { + handle_submit_clone(); + } + Err(_) => { + // Handle error + // We can consider logging or displaying the error + } + } + } + }); }; view! { - <form on:submit=on_submit> + <div> <div class="form-control w-full"> <label class="label"> <span class="label-text">Name</span> @@ -55,7 +90,12 @@ pub fn ExperimentForm( </div> <div class="my-4"> - <ContextForm dimensions=dimensions context=context/> + <ContextForm + dimensions=dimensions + context=context + handle_change=handle_context_form_change + is_standalone=false + /> </div> <div class="form-control w-full"> @@ -67,12 +107,12 @@ pub fn ExperimentForm( // make some more random default id for this can lead to undefined behaviour if we use index based ids <button class="btn btn-outline btn-sm text-xs m-1" - on:click=move |_| { + on:click:undelegated=move |_| { leptos::logging::log!("add new variant"); set_variants - .update(|value| { - let total_variants = value.len(); - value + .update(|curr_variants| { + let total_variants = curr_variants.len(); + curr_variants .push(Variant { id: format!("variant-{}", total_variants), variant_type: VariantType::EXPERIMENTAL, @@ -83,87 +123,88 @@ pub fn ExperimentForm( }); } > - <i class="ri-add-line"></i> Add Variant </button> </div> - <For - each=move || { - variants.get().into_iter().enumerate().collect::<Vec<(usize, Variant)>>() - } + <For + each=move || { + f_variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, Variant)>>() + } + key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) + children=move |(idx, variant)| { + let config = default_config.clone(); + let variant_clone = variant.clone(); + let handle_change = handle_override_form_change(idx); - key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) - children=move |(idx, variant)| { - let config = default_config.clone(); - let variant_clone = variant.clone(); - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=move || variant.variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants - .update(|value| { + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=move || variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants.update(|value| { value[idx] = new_variant; }) - } + } - class="select select-bordered" - > - <option disabled selected> - Pick one - </option> - <option value=VariantType::CONTROL - .to_string()>{VariantType::CONTROL.to_string()}</option> - <option value=VariantType::EXPERIMENTAL - .to_string()> - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> + class="select select-bordered" + > + <option disabled selected> + Pick one + </option> + <option value=VariantType::CONTROL + .to_string()>{VariantType::CONTROL.to_string()}</option> + <option value=VariantType::EXPERIMENTAL + .to_string()> + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> + </div> + </div> + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=config + handle_change=handle_change + is_standalone=false + /> </div> </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=config - /> - </div> - </div> + } } - } - /> - + /> </div> - <div class="modal-action"> - <form method="dialog"> - <button class="btn">Save</button> - </form> + <div class="flex justify-end"> + <button class="btn" on:click:undelegated=on_submit>Save</button> </div> - - </form> + </div> } } \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/mod.rs b/crates/frontend/src/components/experiment_form/mod.rs index ba0d1fc80..39a23b7d7 100644 --- a/crates/frontend/src/components/experiment_form/mod.rs +++ b/crates/frontend/src/components/experiment_form/mod.rs @@ -1 +1,3 @@ pub mod experiment_form; +pub mod types; +pub mod utils; \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs new file mode 100644 index 000000000..06433fdac --- /dev/null +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -0,0 +1,13 @@ +use serde_json::Value; +use serde::Serialize; +use crate::pages::ExperimentList::types::{ + DefaultConfig, Dimension, Variant, VariantType, +}; + +#[derive(Serialize)] +pub struct ExperimentCreateRequest { + pub name: String, + + pub context: Value, + pub variants: Vec<Variant>, +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs new file mode 100644 index 000000000..9065354aa --- /dev/null +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -0,0 +1,36 @@ +use serde_json::{json, Value}; +use crate::pages::ExperimentList::types::Variant; +use super::types::ExperimentCreateRequest; +use crate::components::context_form::utils::construct_context; +use reqwest::StatusCode; + +pub async fn create_experiment( + conditions: Vec<(String, String, String)>, + variants: Vec<Variant>, + name: String, + tenant: String +) -> Result<String, String> { + let payload = ExperimentCreateRequest { + name: name, + variants: variants, + context: construct_context(conditions), + }; + + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/experiments"); + let request_payload = json!(payload); + let response = client + .post(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 1dbda68e6..0b5597343 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -6,3 +6,4 @@ pub mod override_form; pub mod pagination; pub mod side_nav; pub mod table; +pub mod stat; \ No newline at end of file diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 1525fdc7e..0aaa4b1d6 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -11,7 +11,7 @@ pub fn NavItem( let (anchor_class, icon_wrapper_class, icon_class) = if is_active { ( "py-2.5 px-4 flex items-center whitespace-nowrap active".to_string(), - "text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none + "rounded-lg text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 w-8 h-8 flex content-center justify-center pt-0.5 px-1 ".to_string(), format!("{} text-lg text-white font-normal", icon) ) @@ -31,4 +31,4 @@ pub fn NavItem( <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index d169b6aea..5f09f9ff8 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -3,13 +3,18 @@ use leptos::*; use serde_json::{Map, Value, json}; use std::collections::HashSet; use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, HtmlSelectElement}; +use web_sys::{HtmlInputElement, HtmlSelectElement, SubmitEvent, MouseEvent}; #[component] -pub fn OverrideForm( +pub fn OverrideForm<NF>( overrides: Map<String, Value>, default_config: Vec<DefaultConfig>, -) -> impl IntoView { + handle_change: NF, + is_standalone: bool +) -> impl IntoView +where + NF: Fn(Map<String, Value>) + 'static +{ let has_default_config = default_config.len() != 0; let (default_overrides, default_used_config_keys): (Map<String, Value>, Vec<String>) = if overrides.len() == 0 && has_default_config { ( @@ -25,131 +30,151 @@ pub fn OverrideForm( let (overrides, set_overrides) = create_signal(default_overrides); let (used_config_keys, set_used_config_keys) = create_signal(HashSet::from_iter(default_used_config_keys)); - let override_signal = use_context::<RwSignal<(String, Map<String, Value>)>>(); + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("{:?}", overrides.get()); + }; create_effect(move |_| { - let overrides_vec = overrides.get().clone(); - let overrides_map: Map<String, Value> = overrides_vec.into_iter().collect(); - - if let Some(override_context) = override_signal { - override_context.set(( - "SomeName".to_string(), // Adjust according to your needs - overrides_map, - )); - } + let f_override = overrides.get(); + handle_change(f_override.clone()); }); view! { - <div class="space-y-4"> - <div class="flex items-center justify-between gap-4"> - <label class="label"> - <span class="label-text font-semibold text-base">Override</span> - </label> - <div> - <div class="dropdown dropdown-left"> - <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> - <i class="ri-add-line"></i> - Add Config Key - </label> - <ul - tabindex="0" - class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" - > - <Show when=move || !has_default_config>No default config</Show> - <For - each=move || { - default_config - .clone() - .into_iter() - .filter(|item| { - !used_config_keys.get().contains(&item.key) - }) - .collect::<Vec<DefaultConfig>>() - } + <div> + <div class="space-y-4"> + <div class="flex items-center justify-between gap-4"> + <label class="label"> + <span class="label-text font-semibold text-base">Override</span> + </label> + <div> + <div class="dropdown dropdown-left"> + <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> + <i class="ri-add-line"></i> + Add Config Key + </label> + <ul + tabindex="0" + class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" + > + <Show + when = move || !has_default_config + > + No default config + </Show> + <For + each=move || { + default_config + .clone() + .into_iter() + .filter(|item| { + !used_config_keys.get().contains(&item.key) + }) + .collect::<Vec<DefaultConfig>>() + } - key=|item: &DefaultConfig| item.key.to_string() - children=move |item: DefaultConfig| { - let config_key = item.key.to_string(); - let label = config_key.to_string(); - view! { - <li on:click=move |_| { - set_overrides - .update(|value| { - value.insert(config_key.to_string(), json!("")); - }); - set_used_config_keys - .update(|value: &mut HashSet<String>| { - value.insert(config_key.to_string()); - }); - }> + key=|item: &DefaultConfig| item.key.to_string() + children=move |item: DefaultConfig| { + let config_key = item.key.to_string(); + let label = config_key.to_string(); + view! { + <li on:click=move |_| { + set_overrides + .update(|value| { + value + .insert( + config_key.to_string(), + json!("") + ); + }); + set_used_config_keys + .update(|value: &mut HashSet<String>| { + value.insert(config_key.to_string()); + }); + }> - <a>{label.to_string()}</a> - </li> + <a>{label.to_string()}</a> + </li> + } } - } - /> + /> - </ul> + </ul> + </div> </div> </div> - </div> - <For - each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } - key=|(config_key, _)| config_key.to_string() - children=move |(config_key, config_value)| { - let config_key_label = config_key.to_string(); - let config_key_value = config_key.to_string(); - view! { + <Show + when=move || overrides.get().len() == 0 + > + <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> <div> - <div class="flex items-center gap-4"> - <div class="form-control"> - <label class="label font-medium font-mono text-sm"> - <span class="label-text">{config_key_label} ":"</span> - </label> - </div> - <div class="form-control w-2/5"> - <input - type="text" - placeholder="Enter override here" - name="override" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - bind:value=config_value.to_string() - on:input=move |event| { - let input_value = event_target_value(&event); - set_overrides - .update(|curr_overrides| { - curr_overrides - .insert(config_key_value.to_string(), json!(input_value)); - }); - } - /> - - </div> - <div class="w-1/5"> - <button - class="btn btn-ghost btn-circle btn-sm" - on:click=move |ev| { - ev.prevent_default(); - set_overrides - .update(|value| { - value.remove(&config_key); - }); - set_used_config_keys - .update(|value| { + <i class="ri-windy-line text-3xl"></i> + </div> + <div> + <span class="text-semibold">Add config keys</span> + </div> + </div> + </Show> + <For + each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } + key=|(config_key, _)| config_key.to_string() + children=move |(config_key, config_value)| { + let config_key_label = config_key.to_string(); + let config_key_value = config_key.to_string(); + view! { + <div> + <div class="flex items-center gap-4"> + <div class="form-control"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">{config_key_label}":"</span> + </label> + </div> + <div class="form-control w-2/5"> + <input + type="text" + placeholder="Enter override here" + name="override" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + bind:value=config_value.to_string() + on:input=move |event| { + let input_value = event_target_value(&event); + // TODO: validations + set_overrides.update(|curr_overrides| { + curr_overrides.insert(config_key_value.to_string(), json!(input_value)); + }); + } + /> + </div> + <div class="w-1/5"> + <button + class="btn btn-ghost btn-circle btn-sm" + on:click=move |ev| { + ev.prevent_default(); + set_overrides + .update(|value| { + value.remove(&config_key); + }); + set_used_config_keys.update(|value| { value.remove(&config_key); }); - } - > - - <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> - </button> + } + > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> + </div> </div> </div> - </div> + } } - } - /> - + /> + </div> + <Show + when=move || is_standalone + > + <div class="flex justify-end"> + <button class="btn" on:click:undelegated=on_submit>Save</button> + </div> + </Show> </div> } } \ No newline at end of file diff --git a/crates/frontend/src/components/stat/mod.rs b/crates/frontend/src/components/stat/mod.rs new file mode 100644 index 000000000..2b9cad20b --- /dev/null +++ b/crates/frontend/src/components/stat/mod.rs @@ -0,0 +1 @@ +pub mod stat; \ No newline at end of file diff --git a/crates/frontend/src/components/stat/stat.rs b/crates/frontend/src/components/stat/stat.rs new file mode 100644 index 000000000..e2ad5b152 --- /dev/null +++ b/crates/frontend/src/components/stat/stat.rs @@ -0,0 +1,21 @@ +use leptos::*; + +#[component] +pub fn Stat( + heading: &'static str, + icon: &'static str, + number: String, +) -> impl IntoView { + let icon_class = format!("{} text-5xl", icon); + view! { + <div class="stats shadow"> + <div class="stat"> + <div class="stat-figure text-purple-700"> + <i class={icon_class}></i> + </div> + <div class="stat-title">{heading}</div> + <div class="stat-value">{number}</div> + </div> + </div> + } +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 1ff0dd395..b7fe6bb89 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,4 +1,4 @@ -// use std::collections::HashMap; +use std::collections::HashMap; use std::rc::Rc; use crate::api::{fetch_default_config, fetch_dimensions}; @@ -13,7 +13,7 @@ use leptos::*; use leptos_router::use_query_map; use reqwest::{Error, StatusCode}; use serde_json::{json, Map, Value}; -use web_sys::SubmitEvent; +use web_sys::MouseEvent; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); @@ -29,7 +29,12 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { } #[component] -fn ContextModalForm() -> impl IntoView { +fn ContextModalForm<NF>( + handle_change: NF +) -> impl IntoView +where + NF: Fn(Vec<(String, String, String)>) + 'static + Clone +{ let query = use_query_map(); let tenant = query.with(|params_map| { @@ -49,29 +54,37 @@ fn ContextModalForm() -> impl IntoView { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - {move || { - dimensions - .with(move |result| { - match result { - Some(Ok(dimension)) => { - view! { - <div> - <ContextForm dimensions=dimension.clone() context=vec![]/> - </div> + { + let handle_change_clone = handle_change.clone(); + move || { + let handle_change_clone_clone = handle_change_clone.clone(); + dimensions + .with(move |result| { + match result { + Some(Ok(dimension)) => { + view! { + <div> + <ContextForm + dimensions=dimension.clone() + context=vec![] + is_standalone=false + handle_change=handle_change_clone_clone.clone() + /> + </div> + } } - } - Some(Err(error)) => { - view! { - <div class="text-red-500"> - {"Failed to fetch config data: "} {error} - </div> + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + } + } + None => { + view! { <div>Loading....</div> } } } - None => { - view! { <div>Loading....</div> } - } - } - }) + }) }} </Suspense> @@ -147,7 +160,12 @@ pub async fn create_context( } #[component] -fn OverrideModalForm() -> impl IntoView { +fn OverrideModalForm<NF>( + handle_change: NF +) -> impl IntoView +where + NF: Fn(Map<String, Value>) + 'static + Clone +{ let query = use_query_map(); let tenant = query.with(|params_map| { @@ -167,33 +185,39 @@ fn OverrideModalForm() -> impl IntoView { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - {move || { - default_config - .with(move |result| { - match result { - Some(Ok(config)) => { - view! { - <div> - <OverrideForm - overrides=Map::new() - default_config=config.clone() - /> - </div> + { + let handle_change_clone = handle_change.clone(); + move || { + let handle_change_clone_clone = handle_change_clone.clone(); + default_config + .with(move |result| { + match result { + Some(Ok(config)) => { + view! { + <div> + <OverrideForm + overrides=Map::new() + default_config=config.clone() + is_standalone=false + handle_change=handle_change_clone_clone.clone() + /> + </div> + } } - } - Some(Err(error)) => { - view! { - <div class="text-red-500"> - {"Failed to fetch config data: "} {error} - </div> + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error} + </div> + } + } + None => { + view! { <div>Loading....</div> } } } - None => { - view! { <div>Loading....</div> } - } - } - }) - }} + }) + } + } </Suspense> </div> @@ -268,68 +292,32 @@ fn format_condition(condition: &Value) -> String { #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { - let context_data: Vec<(String, String, String, String)> = vec![]; - let condition_ctx: RwSignal<Vec<(String, String, String, String)>> = - create_rw_signal(context_data); - - provide_context(condition_ctx); + let (context_condition, set_context_condition) = create_signal::<Vec<(String, String, String)>>(vec![]); + let (overrides, set_overrides) = create_signal::<Map<String, Value>>(Map::new()); - let ovrride_signal: RwSignal<(String, Map<String, Value>)> = - create_rw_signal(("str".to_string(), Map::new())); - - provide_context(ovrride_signal); - - let (ctx_form, set_ctx_form) = create_signal(vec![( - "dimension1".to_string(), - "operator1".to_string(), - "value1".to_string(), - "string".to_string(), - )]); - - let (ovrride_form, set_ovrride_form) = - create_signal(("dummy".to_string(), Map::new())); + let handle_context_change = move |updated_ctx: Vec<(String, String, String)>| { + set_context_condition.set(updated_ctx); + }; + let handle_overrides_change = move |updated_overrides: Map<String, Value>| { + set_overrides.set(updated_overrides); + }; let (error_message, set_error_message) = create_signal("".to_string()); let on_submit = { - move |ev: SubmitEvent| { + move |ev: MouseEvent| { let handle_submit_clone = handle_submit.clone(); ev.prevent_default(); - set_ctx_form.set(condition_ctx.get()); - let ovrd_values = ovrride_signal.get().1; - // set_ovrride_form.set(ovrride_signal.get()) - set_ovrride_form.set(( - "Override Name".to_string(), - ovrd_values - .into_iter() - .filter_map(|(key, value)| { - let value_clone = value.clone(); - if let Value::String(val) = value { - if !val.is_empty() { - Some((key, value_clone)) - } else { - None - } - } else { - None - } - }) - .collect::<Map<String, Value>>(), - )); - let context_tuples: Vec<(String, String, String)> = ctx_form - .get() - .iter() - .map(|(_dim, dim, op, val)| (dim.clone(), op.clone(), val.clone())) - .collect(); + logging::log!("tirggering submit"); spawn_local({ let handle_submit = handle_submit_clone; async move { let result = create_context( "mjos".to_string(), - ovrride_form.get().1, - context_tuples, + overrides.get(), + context_condition.get(), ) .await; @@ -358,45 +346,37 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { }; view! { - <div class="p-6 text-gray-600 space-y-6"> - <EditButton text="Create Context Overrides".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> - // - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white space-y-6 w-11/12 max-w-3xl"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill"></i> + <dialog id="my_modal_5" class="modal"> + <div class="modal-box relative bg-white w-12/12 max-w-4xl"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + <form + class="form-control w-full mt-8 bg-white text-gray-700 font-mono" + > + <div> + <ContextModalForm handle_change=handle_context_change/> + </div> + <div class="mt-7"> + <OverrideModalForm handle_change=handle_overrides_change/> + </div> + <div class="form-control mt-7"> + <button + class="btn btn-primary shadow-md font-mono" + on:click=on_submit + // onclick="my_modal_5.close()" + > + Submit </button> - </form> - // on:submit=on_submit - <form - class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - on:submit=on_submit - > - <ContextModalForm/> - <OverrideModalForm/> - <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - // onclick="my_modal_5.close()" - > - Submit - </button> - </div> - { - - view! { - <div> - <p class="text-red-500">{move || error_message.get()}</p> - </div> - } - } - - </form> - </div> - </dialog> - </div> + </div> + <div class="mt-7"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + </form> + </div> + </dialog> } } @@ -479,8 +459,16 @@ pub fn ContextOverride() -> impl IntoView { }); view! { - <div class="p-8 space-y-6 bg-gray-120"> - <div class="container mx-auto space-y-6 p-8"> + <div class="p-8"> + <div class="flex justify-between"> + <h2 class="card-title">Overrides</h2> + <EditButton + text="Create Overrides".to_string() + modal= "my_modal_5".to_string() + modalAction = "showModal()".to_string() + /> + </div> + <div class="space-y-6"> <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } @@ -583,4 +571,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 64ca4d54f..ef41909df 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -6,13 +6,15 @@ use crate::components::table::{ types::{Column, TableSettings}, }; use crate::components::Button::EditButton::EditButton; +use crate::components::stat::stat::Stat; use crate::pages::DefaultConfig::types::Config; use js_sys; use leptos::ev::SubmitEvent; use leptos::spawn_local; use leptos::*; use leptos_router::use_query_map; -use serde_json::{Map, Value}; +use serde_json::{Map, Value, json}; +use crate::pages::ExperimentList::utils::fetch_default_config; #[derive(Clone, Debug, Default)] pub struct RowData { @@ -63,19 +65,16 @@ pub async fn create_default_config( #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { - <div class="p-6 bg-white text-gray-600"> - <EditButton text="Create DefaultConfig".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill"></i> - </button> - </form> - <FormComponent handle_submit=handle_submit/> - </div> - </dialog> - </div> + <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> + <div class="modal-box relative bg-white"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + <FormComponent handle_submit=handle_submit/> + </div> + </dialog> } } @@ -213,8 +212,8 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { fn custom_formatter(value: &str, row: &Map<String, Value>) -> View { let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); - let row_key = row["KEY"].clone().to_string().replace("\"", ""); - let row_value = row["VALUE"].clone().to_string().replace("\"", ""); + let row_key = row["key"].clone().to_string().replace("\"", ""); + let row_value = row["value"].clone().to_string().replace("\"", ""); let edit_click_handler = move |_| { let row_data = RowData { @@ -256,80 +255,82 @@ pub fn DefaultConfig() -> impl IntoView { .unwrap_or_else(|| "mjos".to_string()) }); - let config_data = - create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); + let default_config_resource = create_blocking_resource( + || (), + |_| async move { + match fetch_default_config().await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); let table_columns = create_memo(move |_| { vec![ - Column::default("KEY".to_string()), - Column::default("VALUE".to_string()), + Column::default("key".to_string()), + Column::default("schema".to_string()), + Column::default("value".to_string()), + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), Column::new("EDIT".to_string(), None, Some(custom_formatter)), ] }); + view! { <div class="p-8"> - <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> - <Suspense fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - }> - - {move || { - config_data - .with(move |result| { - match result { - Some(Ok(config)) => { - let mut default_config: Vec<Map<String, Value>> = Vec::new(); - let settings = TableSettings { - redirect_prefix: None, - }; - for (key, value) in config.default_configs.iter() { - let mut map = Map::new(); - let trimmed_key = Value::String( - key.trim_matches('"').to_string(), - ); - let formatted_value = Value::String( - format!("{}", value).trim_matches('"').to_string(), - ); - map.insert("KEY".to_string(), trimmed_key); - map.insert("VALUE".to_string(), formatted_value); - default_config.push(map); - } - vec![ - view! { - <div class="card rounded-lg w-full bg-base-100 shadow"> - <div class="card-body"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> - "Default Config" - </h2> - <Table - table_style="font-mono".to_string() - rows=default_config - key_column="id".to_string() - columns=table_columns.get() - settings=settings - /> - </div> + <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> + <Suspense + fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + } + > + { + move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let total_default_config_keys = default_config.len().to_string(); + let table_settings = TableSettings { + redirect_prefix: None + }; - </div> - }, - ] - } - Some(Err(error)) => { - vec![ - view! { - <div class="text-red-500"> - {"Failed to fetch config data: "} {error} - </div> - }, - ] - } - None => vec![view! { <div>Loading....</div> }], - } - }) - }} + let table_rows = default_config + .into_iter() + .map(|config| { json!(config).as_object().unwrap().to_owned() }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat + heading="Config Keys" + icon="ri-tools-line" + number={total_default_config_keys} + /> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Default Config" + </h2> + <EditButton + text="Create DefaultConfig".to_string() + modal= "my_modal_5".to_string() + modalAction = "showModal()".to_string() + /> + </div> + <Table + table_style="font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + settings=table_settings + /> + </div> + </div> + } + } + } </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 68b45cc8d..cfdb3bb16 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,14 +1,18 @@ use std::collections::HashMap; use std::rc::Rc; +use crate::components::{ + stat::stat::Stat, + table::{table::Table, types::{TableSettings, Column}}, +}; + + use crate::components::Button::EditButton::EditButton; use leptos::logging::*; use leptos::*; use serde_json::{json, Map, Value}; use web_sys::SubmitEvent; -use crate::components::table::types::TableSettings; -use crate::components::table::{table::Table, types::Column}; use crate::pages::Dimensions::helper::fetch_dimensions; #[derive(Clone, Debug, Default)] @@ -95,7 +99,6 @@ fn ModalComponent( ) -> impl IntoView { view! { <div class="pt-4"> - <EditButton text="Create Dimension".to_string() modal= "my_modal_5".to_string() modalAction = "showModal()".to_string() /> <FormComponent handle_submit=handle_submit tenant=tenant/> </div> } @@ -288,24 +291,20 @@ pub fn Dimensions() -> impl IntoView { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - <div class="stats shadow"> - <div class="stat"> - <div class="stat-figure text-primary"> - <i class="ri-ruler-2-fill text-5xl"></i> - </div> - <div class="stat-title">Dimensions</div> - - {move || { - let value = dimensions.get(); - let total_items = match value { - Some(v) => v.len(), - _ => 0, - }; - view! { <div class="stat-value">{total_items}</div> } - }} - - </div> - </div> + {move || { + let value = dimensions.get(); + let total_items = match value { + Some(v) => v.len().to_string(), + _ => "0".to_string(), + }; + view! { + <Stat + heading="Dimensions" + icon="ri-ruler-2-fill" + number={total_items} + /> + } + }} <ModalComponent handle_submit=Rc::new(move || dimensions.refetch()) tenant=tenant_rs @@ -314,7 +313,14 @@ pub fn Dimensions() -> impl IntoView { <div class="card rounded-xl w-full bg-base-100 shadow"> <div class="card-body"> - <h2 class="card-title">Dimensions</h2> + <div class="flex justify-between mb-2"> + <h2 class="card-title">Dimensions</h2> + <EditButton + text="Create Dimension".to_string() + modal= "my_modal_5".to_string() + modalAction = "showModal()".to_string() + /> + </div> <div> {move || { @@ -349,4 +355,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index f95f368b8..ee101faba 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -89,7 +89,7 @@ async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, Str .await { Ok(experiment) => { - debug!("experiment response {:?}", experiment); + // debug!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -526,4 +526,4 @@ fn add_dialogs( }.into_view(), ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 67bf5b548..94860a741 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,11 +1,13 @@ use leptos::logging::*; use leptos::*; +use leptos_dom::*; use chrono::{prelude::Utc, TimeZone}; use crate::components::{ experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, + stat::stat::Stat, table::{ table::Table, types::{Column, TableSettings}, @@ -16,6 +18,7 @@ use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; use super::utils::{fetch_default_config, fetch_dimensions, fetch_experiments}; use serde_json::{json, Map, Value}; +use wasm_bindgen::JsCast; #[component] pub fn ExperimentList() -> impl IntoView { @@ -28,6 +31,7 @@ pub fn ExperimentList() -> impl IntoView { page: Some(1), count: Some(10), }); + let (open_form_modal, set_open_form_modal) = create_signal(false); let table_columns = create_memo(move |_| { vec![ @@ -92,36 +96,66 @@ pub fn ExperimentList() -> impl IntoView { }, ); + let handle_submit_experiment_form = move || { + experiments.refetch(); + set_open_form_modal.set(false); + if let Some(element) = document().get_element_by_id("create_exp_modal") { + let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); + match dialog_ele { + Some(ele) => { ele.close(); }, + None => { log!("no modal element"); } + } + } + }; + // TODO: Add filters view! { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - <div class="stats shadow"> - <div class="stat"> - <div class="stat-figure text-primary"> - <i class="ri-test-tube-fill text-5xl"></i> - </div> - <div class="stat-title">Experiments</div> - - {move || { - let value = experiments.get(); - let total_items = match value { - Some(v) => v.total_items, - _ => 0, - }; - view! { <div class="stat-value">{total_items}</div> } - }} + { + move || { + let value = experiments.get(); + let total_items = match value { + Some(v) => v.total_items.to_string(), + _ => "0".to_string(), + }; - </div> - <div class="stat cursor-pointer" onclick="create_exp_modal.showModal()"> - <div class="stat-figure text-primary">new</div> - </div> - </div> + view! { + <Stat + heading="Experiments" + icon="ri-test-tube-fill" + number={total_items} + /> + } + } + } </div> <div class="card rounded-xl w-full bg-base-100 shadow"> <div class="card-body"> - <h2 class="card-title">Experiments</h2> + <div class="flex justify-between"> + <h2 class="card-title">Experiments</h2> + <div> + <button + class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" + on:click=move |event: web_sys::MouseEvent| { + event.prevent_default(); + set_open_form_modal.set(true); + if let Some(element) = document().get_element_by_id("create_exp_modal") { + log!("opening the experiment modal"); + let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); + match dialog_ele { + Some(ele) => { ele.show_modal(); }, + None => { log!("no modal element"); } + } + } + } + > + Create Experiment + <i class="ri-edit-2-line ml-2"></i> + </button> + </div> + </div> <div> {move || { @@ -201,21 +235,48 @@ pub fn ExperimentList() -> impl IntoView { {move || { let dim = dimensions.get().unwrap_or(vec![]); let def_conf = default_config.get().unwrap_or(vec![]); + let open_modal = open_form_modal.get(); view! { - <dialog id="create_exp_modal" class="modal"> - <div class="modal-box w-12/12 max-w-5xl"> - <h3 class="font-bold text-lg">Create Experiment</h3> - <div class="modal-action flex flex-col"> - <ExperimentForm - name="".to_string() - context=vec![] - variants=vec![] - dimensions=dim - default_config=def_conf - /> + <Show + when=move || { open_form_modal.get() } + > + <dialog id="create_exp_modal" class="modal"> + <div class="modal-box w-12/12 max-w-5xl"> + <div class="flex justify-between"> + <h3 class="font-bold text-lg">Create Experiment</h3> + <div> + <button + on:click=move |_| { + set_open_form_modal.set(false); + if let Some(element) = document().get_element_by_id("create_exp_modal") { + log!("FOUND AND CLOSING THE FORM"); + let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); + match dialog_ele { + Some(ele) => { ele.close(); }, + None => { log!("no modal element"); } + } + } else { + log!("outer close button no modal element"); + } + } + > + <i class="ri-close-fill"></i> + </button> + </div> + </div> + <div class="modal-action flex flex-col"> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim.clone() + default_config=def_conf.clone() + handle_submit=handle_submit_experiment_form + /> + </div> </div> - </div> - </dialog> + </dialog> + </Show> } }} diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index 58b80995a..34b558f18 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -74,11 +74,11 @@ pub enum VariantType { EXPERIMENTAL, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct Variant { pub id: String, pub variant_type: VariantType, pub context_id: Option<String>, pub override_id: Option<String>, pub overrides: Map<String, Value>, -} +} \ No newline at end of file diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index a9f938453..6799796a5 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash TENANT=$1 DB_URL=$2 From 93a3abd188347e6b5054bd993ca23845553d482d Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 14 Dec 2023 10:45:48 +0530 Subject: [PATCH 212/352] feat: fixed experiment suspense block , added generic button --- .env.example | 2 +- .../frontend/src/components/Button/Button.rs | 17 +++ .../src/components/Button/EditButton.rs | 11 -- crates/frontend/src/components/Button/mod.rs | 2 +- .../experiment_form/experiment_form.rs | 21 ++-- crates/frontend/src/lib.rs | 3 +- .../pages/ContextOverride/ContextOverride.rs | 44 +++---- .../src/pages/DefaultConfig/DefaultConfig.rs | 25 ++-- .../src/pages/Dimensions/Dimensions.rs | 26 ++-- .../pages/ExperimentList/ExperimentList.rs | 117 +++++++++++------- crates/frontend/src/pages/Home/Home.rs | 2 +- crates/frontend/src/utils.rs | 21 ++++ makefile | 2 +- 13 files changed, 160 insertions(+), 133 deletions(-) create mode 100644 crates/frontend/src/components/Button/Button.rs delete mode 100644 crates/frontend/src/components/Button/EditButton.rs create mode 100644 crates/frontend/src/utils.rs diff --git a/.env.example b/.env.example index 72de84f5b..bb781fde4 100644 --- a/.env.example +++ b/.env.example @@ -25,4 +25,4 @@ TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/,/default-config" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" -SERVICE_NAME="CAC" +SERVICE_NAME="CAC" \ No newline at end of file diff --git a/crates/frontend/src/components/Button/Button.rs b/crates/frontend/src/components/Button/Button.rs new file mode 100644 index 000000000..ba5072593 --- /dev/null +++ b/crates/frontend/src/components/Button/Button.rs @@ -0,0 +1,17 @@ +use std::rc::Rc; + +use leptos::*; +use web_sys::MouseEvent; + +#[component] +pub fn Button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl IntoView { + view! { + <button + class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" + on:click=on_click + > + {text} + <i class="ri-edit-2-line ml-2"></i> + </button> + } +} diff --git a/crates/frontend/src/components/Button/EditButton.rs b/crates/frontend/src/components/Button/EditButton.rs deleted file mode 100644 index a66a2f2fd..000000000 --- a/crates/frontend/src/components/Button/EditButton.rs +++ /dev/null @@ -1,11 +0,0 @@ -use leptos::*; - -#[component] -pub fn EditButton(text: String, modal: String, modalAction: String) -> impl IntoView { - view! { - <button class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" onclick=format!("{modal}.{modalAction}")> - {text} - <i class="ri-edit-2-line ml-2"></i> - </button> - } -} diff --git a/crates/frontend/src/components/Button/mod.rs b/crates/frontend/src/components/Button/mod.rs index e52febfcf..62044bd9b 100644 --- a/crates/frontend/src/components/Button/mod.rs +++ b/crates/frontend/src/components/Button/mod.rs @@ -1 +1 @@ -pub mod EditButton; +pub mod Button; diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 25aae9518..01383efe1 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,15 +1,16 @@ +use super::utils::create_experiment; +use crate::components::Button::Button::Button; use crate::components::{ context_form::context_form::ContextForm, override_form::override_form::OverrideForm, }; use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; -use super::utils::create_experiment; use leptos::*; -use serde_json::{Value, Map}; -use wasm_bindgen::JsCast; -use web_sys::{SubmitEvent, HtmlInputElement, MouseEvent}; +use serde_json::{Map, Value}; use std::sync::RwLock; +use wasm_bindgen::JsCast; +use web_sys::{HtmlInputElement, MouseEvent, SubmitEvent}; #[component] pub fn ExperimentForm<NF>( @@ -18,10 +19,10 @@ pub fn ExperimentForm<NF>( variants: Vec<Variant>, dimensions: Vec<Dimension>, default_config: Vec<DefaultConfig>, - handle_submit: NF + handle_submit: NF, ) -> impl IntoView where - NF: Fn() + 'static + Clone + NF: Fn() + 'static + Clone, { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (experiment_name, set_experiment_name) = create_signal(name); @@ -57,7 +58,9 @@ where spawn_local({ async move { - let result = create_experiment(f_context, f_variants, f_experiment_name, tenant).await; + let result = + create_experiment(f_context, f_variants, f_experiment_name, tenant) + .await; match result { Ok(value) => { @@ -203,8 +206,8 @@ where /> </div> <div class="flex justify-end"> - <button class="btn" on:click:undelegated=on_submit>Save</button> + <Button text="Submit".to_string() on_click = on_submit /> </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 21989618e..21371bd1d 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -1,9 +1,10 @@ +mod api; pub mod app; pub mod components; pub mod hoc; pub mod pages; pub mod types; -mod api; +mod utils; use cfg_if::cfg_if; cfg_if! { diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index b7fe6bb89..c55e72af8 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -6,14 +6,16 @@ use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; -use crate::components::Button::EditButton::EditButton; +use crate::components::Button::Button::Button; use crate::pages::DefaultConfig::types::Config; // use leptos::spawn_local; +use crate::utils::modal_action; use leptos::*; use leptos_router::use_query_map; use reqwest::{Error, StatusCode}; use serde_json::{json, Map, Value}; -use web_sys::MouseEvent; +use wasm_bindgen::JsCast; +use web_sys::{HtmlDialogElement, MouseEvent}; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); @@ -29,11 +31,9 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { } #[component] -fn ContextModalForm<NF>( - handle_change: NF -) -> impl IntoView +fn ContextModalForm<NF>(handle_change: NF) -> impl IntoView where - NF: Fn(Vec<(String, String, String)>) + 'static + Clone + NF: Fn(Vec<(String, String, String)>) + 'static + Clone, { let query = use_query_map(); @@ -160,11 +160,9 @@ pub async fn create_context( } #[component] -fn OverrideModalForm<NF>( - handle_change: NF -) -> impl IntoView +fn OverrideModalForm<NF>(handle_change: NF) -> impl IntoView where - NF: Fn(Map<String, Value>) + 'static + Clone + NF: Fn(Map<String, Value>) + 'static + Clone, { let query = use_query_map(); @@ -292,7 +290,8 @@ fn format_condition(condition: &Value) -> String { #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { - let (context_condition, set_context_condition) = create_signal::<Vec<(String, String, String)>>(vec![]); + let (context_condition, set_context_condition) = + create_signal::<Vec<(String, String, String)>>(vec![]); let (overrides, set_overrides) = create_signal::<Map<String, Value>>(Map::new()); let handle_context_change = move |updated_ctx: Vec<(String, String, String)>| { @@ -324,12 +323,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { match result { Ok(str) => { handle_submit(); - // log!("Hi babe{str}"); - - js_sys::eval( - "document.getElementById('my_modal_5').close();", - ) - .unwrap(); + modal_action("my_modal_5", "close") } Err(e) => { if e.is_empty() { @@ -363,13 +357,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <OverrideModalForm handle_change=handle_overrides_change/> </div> <div class="form-control mt-7"> - <button - class="btn btn-primary shadow-md font-mono" - on:click=on_submit - // onclick="my_modal_5.close()" - > - Submit - </button> + <Button text="Submit".to_string() on_click = move |ev:MouseEvent| on_submit(ev) /> </div> <div class="mt-7"> <p class="text-red-500">{move || error_message.get()}</p> @@ -462,11 +450,7 @@ pub fn ContextOverride() -> impl IntoView { <div class="p-8"> <div class="flex justify-between"> <h2 class="card-title">Overrides</h2> - <EditButton - text="Create Overrides".to_string() - modal= "my_modal_5".to_string() - modalAction = "showModal()".to_string() - /> + <Button text="Create Context Overrides".to_string() on_click= |_| modal_action("my_modal_5","open") /> </div> <div class="space-y-6"> <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> @@ -571,4 +555,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index ef41909df..b36d26a8f 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -5,16 +5,18 @@ use crate::components::table::{ table::Table, types::{Column, TableSettings}, }; -use crate::components::Button::EditButton::EditButton; + use crate::components::stat::stat::Stat; +use crate::components::Button::Button::Button; use crate::pages::DefaultConfig::types::Config; +use crate::pages::ExperimentList::utils::fetch_default_config; +use crate::utils::modal_action; use js_sys; use leptos::ev::SubmitEvent; use leptos::spawn_local; use leptos::*; use leptos_router::use_query_map; -use serde_json::{Map, Value, json}; -use crate::pages::ExperimentList::utils::fetch_default_config; +use serde_json::{json, Map, Value}; #[derive(Clone, Debug, Default)] pub struct RowData { @@ -198,13 +200,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { /> </div> <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" - > - Submit - </button> + <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> </div> </form> } @@ -276,7 +272,6 @@ pub fn DefaultConfig() -> impl IntoView { ] }); - view! { <div class="p-8"> <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> @@ -312,11 +307,7 @@ pub fn DefaultConfig() -> impl IntoView { <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> "Default Config" </h2> - <EditButton - text="Create DefaultConfig".to_string() - modal= "my_modal_5".to_string() - modalAction = "showModal()".to_string() - /> + <Button text="Create Default Config".to_string() on_click= |_| modal_action("my_modal_5","open") /> </div> <Table table_style="font-mono".to_string() @@ -333,4 +324,4 @@ pub fn DefaultConfig() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index cfdb3bb16..8c99dffaf 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,13 +1,15 @@ use std::collections::HashMap; use std::rc::Rc; +use crate::components::Button::Button::Button; use crate::components::{ stat::stat::Stat, - table::{table::Table, types::{TableSettings, Column}}, + table::{ + table::Table, + types::{Column, TableSettings}, + }, }; - - -use crate::components::Button::EditButton::EditButton; +use crate::utils::modal_action; use leptos::logging::*; use leptos::*; use serde_json::{json, Map, Value}; @@ -236,13 +238,7 @@ fn FormComponent( /> </div> <div class="form-control mt-6"> - <button - type="submit" - class="btn btn-primary shadow-md font-mono" - onclick="my_modal_5.close()" - > - Submit - </button> + <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> </div> </form> </div> @@ -315,11 +311,7 @@ pub fn Dimensions() -> impl IntoView { <div class="card-body"> <div class="flex justify-between mb-2"> <h2 class="card-title">Dimensions</h2> - <EditButton - text="Create Dimension".to_string() - modal= "my_modal_5".to_string() - modalAction = "showModal()".to_string() - /> + <Button text="Create Dimension".to_string() on_click= |_| modal_action("my_modal_5","open") /> </div> <div> @@ -355,4 +347,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 94860a741..f34534f6a 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,8 +1,10 @@ +use futures::join; use leptos::logging::*; use leptos::*; use leptos_dom::*; use chrono::{prelude::Utc, TimeZone}; +use serde::{Deserialize, Serialize}; use crate::components::{ experiment_form::experiment_form::ExperimentForm, @@ -14,9 +16,19 @@ use crate::components::{ }, }; +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + experiments: ExperimentsResponse, + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, +} + use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; -use super::utils::{fetch_default_config, fetch_dimensions, fetch_experiments}; +use super::{ + types::{DefaultConfig, Dimension}, + utils::{fetch_default_config, fetch_dimensions, fetch_experiments}, +}; use serde_json::{json, Map, Value}; use wasm_bindgen::JsCast; @@ -62,48 +74,45 @@ pub fn ExperimentList() -> impl IntoView { ] }); - let experiments = create_blocking_resource( - move || (tenant_rs.get(), filters.get()), - |(current_tenant, value)| async move { - match fetch_experiments(value, ¤t_tenant).await { - Ok(data) => data, - Err(_) => ExperimentsResponse { - total_items: 0, - total_pages: 0, - data: vec![], - }, - } - }, - ); + let combined_resource: Resource<(String, ListFilters), CombinedResource> = + create_blocking_resource( + move || (tenant_rs.get(), filters.get()), + |(current_tenant, filters)| async move { + // Perform all fetch operations concurrently + let experiments_future = fetch_experiments(filters, ¤t_tenant); + let dimensions_future = fetch_dimensions(); + let config_future = fetch_default_config(); - let dimensions = create_blocking_resource( - || (), - |_| async move { - match fetch_dimensions().await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); + let (experiments_result, dimensions_result, config_result) = + join!(experiments_future, dimensions_future, config_future); - let default_config = create_blocking_resource( - || (), - |_| async move { - match fetch_default_config().await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); + // Construct the combined result, handling errors as needed + CombinedResource { + experiments: experiments_result.unwrap_or_else(|_| { + ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + } + }), + dimensions: dimensions_result.unwrap_or_else(|_| vec![]), + default_config: config_result.unwrap_or_else(|_| vec![]), + } + }, + ); let handle_submit_experiment_form = move || { - experiments.refetch(); + combined_resource.refetch(); set_open_form_modal.set(false); if let Some(element) = document().get_element_by_id("create_exp_modal") { let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); match dialog_ele { - Some(ele) => { ele.close(); }, - None => { log!("no modal element"); } + Some(ele) => { + ele.close(); + } + None => { + log!("no modal element"); + } } } }; @@ -115,9 +124,9 @@ pub fn ExperimentList() -> impl IntoView { <div class="pb-4"> { move || { - let value = experiments.get(); + let value = combined_resource.get(); let total_items = match value { - Some(v) => v.total_items.to_string(), + Some(v) => v.experiments.total_items.to_string(), _ => "0".to_string(), }; @@ -165,10 +174,11 @@ pub fn ExperimentList() -> impl IntoView { format!("admin/{current_tenant}/experiments"), ), }; - let value = experiments.get(); + let value = combined_resource.get(); match value { Some(v) => { let data = v + .experiments .data .iter() .map(|ele| { json!(ele).as_object().unwrap().clone() }) @@ -193,8 +203,8 @@ pub fn ExperimentList() -> impl IntoView { {move || { let current_page = filters.get().page.unwrap_or(0); - let total_pages = match experiments.get() { - Some(val) => val.total_pages, + let total_pages = match combined_resource.get() { + Some(val) => val.experiments.total_pages, None => 0, }; view! { @@ -233,9 +243,28 @@ pub fn ExperimentList() -> impl IntoView { </div> {move || { - let dim = dimensions.get().unwrap_or(vec![]); - let def_conf = default_config.get().unwrap_or(vec![]); - let open_modal = open_form_modal.get(); + let dim = combined_resource.get().unwrap_or(CombinedResource { + experiments: + ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + } + , + dimensions: vec![], + default_config: vec![], + }).dimensions; + let def_conf = combined_resource.get().unwrap_or(CombinedResource { + experiments: + ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + } + , + dimensions: vec![], + default_config: vec![], + }).default_config; view! { <Show when=move || { open_form_modal.get() } @@ -283,4 +312,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 6bc1df05f..80ac67d56 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -385,4 +385,4 @@ pub fn home() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs new file mode 100644 index 000000000..a64ef7778 --- /dev/null +++ b/crates/frontend/src/utils.rs @@ -0,0 +1,21 @@ +use leptos::*; +use wasm_bindgen::JsCast; + +pub fn modal_action(name: &str, action: &str) { + if let Some(window) = web_sys::window() { + if let Some(document) = window.document() { + if let Some(modal) = document.get_element_by_id(name) { + logging::log!("Modal found"); + if let Some(el) = modal.dyn_ref::<web_sys::HtmlDialogElement>() { + if action == "close" { + el.close(); + } else { + el.show_modal(); + } + } + } else { + logging::log!("Modal element not found"); + } + } + } +} diff --git a/makefile b/makefile index 086734c04..688f31942 100644 --- a/makefile +++ b/makefile @@ -100,7 +100,7 @@ run: sleep 0.5; \ done # make setup - # cp .env.example .env + cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env make cac -e DOCKER_DNS=$(DOCKER_DNS) From 27fbb995155342b56f930aef2c27e8efd40aea0c Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Fri, 8 Dec 2023 13:42:16 +0530 Subject: [PATCH 213/352] feat: working resolve page --- crates/cac_client/src/eval.rs | 40 ++++ crates/cac_client/src/lib.rs | 1 + .../src/api/config/handlers.rs | 31 ++- .../experimentation-platform/src/db/schema.rs | 38 +++- .../components/context_form/context_form.rs | 26 ++- .../experiment_form/experiment_form.rs | 137 +++++++------ .../components/override_form/override_form.rs | 51 +++-- .../pages/ContextOverride/ContextOverride.rs | 119 +++++++---- crates/frontend/src/pages/Experiment/mod.rs | 21 +- crates/frontend/src/pages/Home/Home.rs | 185 ++++++++++++------ 10 files changed, 429 insertions(+), 220 deletions(-) diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index 63658f604..c228ac0fc 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -54,3 +54,43 @@ pub fn eval_cac( let overriden_config = default_config; Ok(overriden_config) } + +pub fn eval_cac_with_reasoning( + mut default_config: Map<String, Value>, + contexts: &Vec<Context>, + overrides: &Map<String, Value>, + query_data: &Map<String, Value>, +) -> Result<Map<String, Value>, String> { + let mut required_overrides: Value = json!({}); + let mut reasoning: Vec<Value> = vec![]; + + for context in contexts.iter() { + if let Ok(Value::Bool(true)) = + jsonlogic::apply(&context.condition, &json!(query_data)) + { + for override_key in &context.override_with_keys { + if let Some(overriden_value) = overrides.get(override_key) { + json_patch::merge(&mut required_overrides, overriden_value); + reasoning.push(json!({ + "context": context.condition, + "override": context.override_with_keys + })); + } + } + } + } + + let applied_overrides: Map<String, Value> = + serde_json::from_value(required_overrides).map_err_to_string()?; + + applied_overrides.into_iter().for_each(|(key, val)| { + if let Some(og_val) = default_config.get_mut(&key) { + json_patch::merge(og_val, &val) + } else { + log::error!("CAC: found non-default_config key: {key} in overrides"); + } + }); + let mut overriden_config = default_config; + overriden_config.insert("metadata".into(), json!(reasoning)); + Ok(overriden_config) +} diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index adc457beb..f0302e05e 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -211,3 +211,4 @@ pub static CLIENT_FACTORY: Lazy<ClientFactory> = Lazy::new(|| ClientFactory(RwLock::new(HashMap::new()))); pub use eval::eval_cac; +pub use eval::eval_cac_with_reasoning; diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 9576cd7a6..e76d79b4f 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -8,7 +8,7 @@ use actix_http::header::{HeaderName, HeaderValue}; use actix_web::{ error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope, }; -use cac_client::eval_cac; +use cac_client::{eval_cac, eval_cac_with_reasoning}; use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; use diesel::{ dsl::max, @@ -177,15 +177,28 @@ async fn get_resolved_config( }) .collect(); - let response = HttpResponse::Ok().json( - eval_cac( - res.default_configs, - &cac_client_contexts, - &res.overrides, - &query_params_map, + let response = if let Some(Value::String(_)) = query_params_map.get("show_reasoning") + { + HttpResponse::Ok().json( + eval_cac_with_reasoning( + res.default_configs, + &cac_client_contexts, + &res.overrides, + &query_params_map, + ) + .map_err_to_internal_server("cac eval failed", Null)?, ) - .map_err_to_internal_server("cac eval failed", Null)?, - ); + } else { + HttpResponse::Ok().json( + eval_cac( + res.default_configs, + &cac_client_contexts, + &res.overrides, + &query_params_map, + ) + .map_err_to_internal_server("cac eval failed", Null)?, + ) + }; add_last_modified_header(max_created_at, response) } diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 143ddfa86..c10c6d338 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -6,6 +6,39 @@ pub mod sql_types { pub struct ExperimentStatusType; } +diesel::table! { + contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + priority -> Int4, + #[sql_name = "override"] + override_ -> Json, + } +} + +diesel::table! { + default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } +} + +diesel::table! { + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + } +} + diesel::table! { event_log (id, timestamp) { id -> Uuid, @@ -196,6 +229,9 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, event_log, event_log_y2023m08, event_log_y2023m09, @@ -210,4 +246,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2024m06, event_log_y2024m07, experiments, -); +); \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 0b18c3b31..728be09e3 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -120,6 +120,7 @@ where .enumerate() .collect::<Vec<(usize, (String, String, String))>>() } + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) children=move |(idx, (dimension, operator, value))| { let dimension_label = dimension.to_string(); @@ -134,11 +135,13 @@ where bind:value=operator on:input=move |event| { let input_value = event_target_value(&event); - set_context.update(|curr_context| { - // setting operator - curr_context[idx].1 = input_value; - }); + set_context + .update(|curr_context| { + curr_context[idx].1 = input_value; + }); } + + name="context-dimension-operator" class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" > <option disabled selected> @@ -152,17 +155,22 @@ where </div> <div class="form-control"> <label class="label capitalize font-mono text-sm"> - <span class="label-text">{dimension_label}</span> + <span class="label-text" name="context-dimension-name"> + {dimension_label} + </span> </label> <div class="flex gap-x-6 items-center"> <input bind:value=value on:input=move |event| { let input_value = event_target_value(&event); - set_context.update(|curr_context| { - curr_context[idx].2 = input_value; - }); + set_context + .update(|curr_context| { + curr_context[idx].2 = input_value; + }); } + + name="context-dimension-value" type="text" placeholder="Type here" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -180,6 +188,7 @@ where }); } > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> </button> </div> @@ -201,6 +210,7 @@ where } } /> + </div> </div> <Show diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 01383efe1..3ca3be964 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -126,84 +126,83 @@ where }); } > + <i class="ri-add-line"></i> Add Variant </button> </div> - <For - each=move || { - f_variants - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, Variant)>>() - } - key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) - children=move |(idx, variant)| { - let config = default_config.clone(); - let variant_clone = variant.clone(); - let handle_change = handle_override_form_change(idx); - - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=move || variant.variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants.update(|value| { + <For + each=move || { + f_variants.get().into_iter().enumerate().collect::<Vec<(usize, Variant)>>() + } + + key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) + children=move |(idx, variant)| { + let config = default_config.clone(); + let variant_clone = variant.clone(); + let handle_change = handle_override_form_change(idx); + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=move || variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants + .update(|value| { value[idx] = new_variant; }) - } - - class="select select-bordered" - > - <option disabled selected> - Pick one - </option> - <option value=VariantType::CONTROL - .to_string()>{VariantType::CONTROL.to_string()}</option> - <option value=VariantType::EXPERIMENTAL - .to_string()> - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> - </div> - </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=config - handle_change=handle_change - is_standalone=false - /> + } + + class="select select-bordered" + > + <option disabled selected> + Pick one + </option> + <option value=VariantType::CONTROL + .to_string()>{VariantType::CONTROL.to_string()}</option> + <option value=VariantType::EXPERIMENTAL + .to_string()> + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> </div> </div> - } + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=config + handle_change=handle_change + is_standalone=false + /> + </div> + </div> } - /> + } + /> + </div> <div class="flex justify-end"> <Button text="Submit".to_string() on_click = on_submit /> diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 5f09f9ff8..4ad75187d 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -57,11 +57,7 @@ where tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" > - <Show - when = move || !has_default_config - > - No default config - </Show> + <Show when=move || !has_default_config>No default config</Show> <For each=move || { default_config @@ -81,11 +77,7 @@ where <li on:click=move |_| { set_overrides .update(|value| { - value - .insert( - config_key.to_string(), - json!("") - ); + value.insert(config_key.to_string(), json!("")); }); set_used_config_keys .update(|value: &mut HashSet<String>| { @@ -126,24 +118,26 @@ where <div class="flex items-center gap-4"> <div class="form-control"> <label class="label font-medium font-mono text-sm"> - <span class="label-text">{config_key_label}":"</span> + <span class="label-text">{config_key_label} ":"</span> </label> </div> <div class="form-control w-2/5"> - <input - type="text" - placeholder="Enter override here" - name="override" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - bind:value=config_value.to_string() - on:input=move |event| { - let input_value = event_target_value(&event); - // TODO: validations - set_overrides.update(|curr_overrides| { - curr_overrides.insert(config_key_value.to_string(), json!(input_value)); + <input + type="text" + placeholder="Enter override here" + name="override" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + bind:value=config_value.to_string() + on:input=move |event| { + let input_value = event_target_value(&event); + set_overrides + .update(|curr_overrides| { + curr_overrides + .insert(config_key_value.to_string(), json!(input_value)); }); - } - /> + } + /> + </div> <div class="w-1/5"> <button @@ -154,11 +148,13 @@ where .update(|value| { value.remove(&config_key); }); - set_used_config_keys.update(|value| { - value.remove(&config_key); - }); + set_used_config_keys + .update(|value| { + value.remove(&config_key); + }); } > + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> </button> </div> @@ -167,6 +163,7 @@ where } } /> + </div> <Show when=move || is_standalone diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index c55e72af8..2c6b96bbb 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::api::{fetch_default_config, fetch_dimensions}; +use crate::api::{fetch_dimensions, fetch_default_config}; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::types::TableSettings; @@ -85,7 +85,8 @@ where } } }) - }} + } + } </Suspense> </div> @@ -468,12 +469,10 @@ pub fn ContextOverride() -> impl IntoView { redirect_prefix: None, }; let mut context_views = Vec::new(); - // let mut new_ctx: Vec<(String, String, String)> = vec![]; let mut override_signal = Map::new(); for context in config.contexts.iter() { let condition = extract_and_format(&context.condition); let ctx_values = parse_conditions(condition.clone()); - // new_ctx.extend(ctx_values.clone()); for key in context.override_with_keys.iter() { let mut map = Map::new(); let ovr = config.overrides.get(key).unwrap(); @@ -495,58 +494,110 @@ pub fn ContextOverride() -> impl IntoView { context_views .push( view! { + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6 shadow-md"> - <div class="flex justify-between"> - <div class="flex items-center space-x-4"> - - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white shadow-md font-mono"> - "Condition" - </h2> - <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> - {ctx_values.into_iter().map(|(dim,op,val)| view!{ - <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> - <span class="font-mono font-medium context_condition text-gray-500">{dim}</span> - <span class="font-mono font-medium text-gray-650 context_condition ">{op}</span> - <span class="font-mono font-semibold context_condition">{val}</span> - </span> - - }).collect::<Vec<_>>()} - </div> - <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> + <div class="flex justify-between"> + <div class="flex items-center space-x-4"> + + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white shadow-md font-mono"> + "Condition" + </h2> + <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> + {ctx_values + .into_iter() + .map(|(dim, op, val)| { + view! { + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> + <span class="font-mono font-medium context_condition text-gray-500"> + {dim} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {op} + </span> + <span class="font-mono font-semibold context_condition"> + {val} + </span> + </span> + } + }) + .collect::<Vec<_>>()} + </div> + <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> <i class="ri-edit-line text-blue-500"></i> </button> - </div> - <div class="space-x-4"> - <Table - table_style="font-mono".to_string() - rows=contexts.clone() - key_column="id".to_string() - columns=table_columns.get() - settings=settings.clone() - /> - </div> - + </div> + <div class="space-x-4"> + <Table + table_style="font-mono".to_string() + rows=contexts.clone() + key_column="id".to_string() + columns=table_columns.get() + settings=settings.clone() + /> + </div> </div> }, ); contexts.clear(); } - // ctx.set(new_ctx); ovr_data.set(override_signal); context_views } Some(Err(error)) => { vec![ view! { + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // ctx.set(new_ctx); + <div class="text-red-500"> {"Failed to fetch config data: "} {error} </div> }, ] } - None => vec![view! { <div>Loading....</div> }], + None => { + vec![ + view! { + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // ctx.set(new_ctx); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // ctx.set(new_ctx); + + // let mut new_ctx: Vec<(String, String, String)> = vec![]; + // new_ctx.extend(ctx_values.clone()); + + // ctx.set(new_ctx); + + <div>Loading....</div> + }, + ] + } } }) }} diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index ee101faba..88c25b064 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -69,7 +69,7 @@ async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, .await { Ok(experiment) => { - // debug!("experiment response {:?}", experiment); + debug!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -89,7 +89,7 @@ async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, Str .await { Ok(experiment) => { - // debug!("experiment response {:?}", experiment); + debug!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -417,14 +417,13 @@ fn add_dialogs( <dialog id="edit_exp_modal" class="modal"> <div class="modal-box"> <h3 class="font-bold text-lg">Edit Experiment</h3> - <div class="modal-action"> - // <ExperimentForm - // name=experiment_rs.get().name - // context=vec![] - // variants=vec![] - // dimensions=dimensions.get().unwrap_or(vec![]) - // default_config=default_config.get().unwrap_or(vec![]) - // /> + // <ExperimentForm + // name=experiment_rs.get().name + <div class="modal-action">// context=vec![] + // variants=vec![] + // dimensions=dimensions.get().unwrap_or(vec![]) + // default_config=default_config.get().unwrap_or(vec![]) + // /> </div> </div> </dialog> @@ -526,4 +525,4 @@ fn add_dialogs( }.into_view(), ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 80ac67d56..4e459423f 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -34,7 +34,8 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); - let url = format!("http://localhost:8080/config/resolve?{context}"); + let url = + format!("http://localhost:8080/config/resolve?{context}&show_reasoning=true"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { let config = response.json().await.map_err(|e| e.to_string())?; @@ -110,6 +111,10 @@ fn format_condition(condition: &Value) -> String { "Invalid Condition".to_string() } +fn gen_name_id(s0: &String, s1: &String, s2: &String) -> String { + format!("{s0}::{s1}::{s2}") +} + #[component] pub fn home() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); @@ -127,7 +132,55 @@ pub fn home() -> impl IntoView { }, ); - let gen_name_id = |s1: &String, s2: &String| format!("{s1}::{s2}"); + let unstrike = |search_field_prefix: &String, config: &Map<String, Value>| { + for (dimension, value) in config.into_iter() { + let search_field_prefix = if search_field_prefix.is_empty() { + dimension + } else { + &search_field_prefix + }; + let search_field_prefix = gen_name_id( + search_field_prefix, + dimension, + &value.as_str().unwrap().to_string(), + ); + logging::log!("search field prefix {:#?}", search_field_prefix); + let config_name_elements = document() + .get_elements_by_name(format!("{search_field_prefix}-1").as_str()); + let config_value_elements = document() + .get_elements_by_name(format!("{search_field_prefix}-2").as_str()); + logging::log!("config_name_elements {:#?}", config_name_elements.length()); + logging::log!( + "config_value_elements {:#?}", + config_value_elements.length() + ); + for i in 0..config_name_elements.length() { + let item_one = config_name_elements.item(i).expect("missing span"); + let item_two = config_value_elements.item(i).expect("missing span"); + + let (config_name_element, config_value_element) = ( + item_one.dyn_ref::<HtmlSpanElement>().unwrap(), + item_two.dyn_ref::<HtmlSpanElement>().unwrap(), + ); + let (name, value) = ( + config_name_element + .inner_html() + .replace("<span class=\"text-green-600\">", "") + .replace("<span class=\"text-orange-600\">", "") + .replace("</span>", ""), + config_value_element + .inner_html() + .replace("<span class=\"text-green-600\">", "") + .replace("<span class=\"text-orange-600\">", "") + .replace("</span>", ""), + ); + + logging::log!("config name after replace {} and value {}", name, value); + config_name_element.set_inner_html(format!("<span class=\"text-green-600\">{}</span>", name).as_str()); + config_value_element.set_inner_html(format!("<span class=\"text-green-600\">{}</span>", value).as_str()); + } + } + }; let gen_query_context = |query: Vec<(String, String, String)>| -> String { let mut context: Vec<String> = vec![]; @@ -136,7 +189,11 @@ pub fn home() -> impl IntoView { "==" => "=", _ => break, // query params do not support the other operators : != and IN, do something differently later }; - context.push(format!("{}{op}{}", dimension.to_lowercase(), value.to_lowercase())); + context.push(format!( + "{}{op}{}", + dimension.to_lowercase(), + value.to_lowercase() + )); } context.join("&").to_string() }; @@ -179,21 +236,23 @@ pub fn home() -> impl IntoView { ); config_name_element.set_inner_html( format!( - "<s>{}</s>", + "<span class=\"text-orange-600\">{}</span>", config_name_element .inner_html() - .replace("<s>", "") - .replace("</s>", "") + .replace("<span class=\"text-green-600\">", "") + .replace("<span class=\"text-orange-600\">", "") + .replace("</span>", "") ) .as_str(), ); config_value_element.set_inner_html( format!( - "<s>{}</s>", + "<span class=\"text-orange-600\">{}</span>", config_value_element .inner_html() - .replace("<s>", "") - .replace("</s>", "") + .replace("<span class=\"text-green-600\">", "") + .replace("<span class=\"text-orange-600\">", "") + .replace("</span>", ""), ) .as_str(), ); @@ -202,42 +261,28 @@ pub fn home() -> impl IntoView { // resolve the context and get the config that would apply spawn_local(async move { let context = gen_query_context(query_vector); - let config = match resolve_config(tenant_rs.get(), context).await.unwrap() { + let mut config = match resolve_config(tenant_rs.get(), context).await.unwrap() + { Value::Object(m) => m, _ => Map::new(), }; + logging::log!("resolved config {:#?}", config); // unstrike those that we want to show the user - for (dimension, value) in config.into_iter() { - let search_field: String = - gen_name_id(&dimension, &value.as_str().unwrap().to_string()); - logging::log!("gen ID search param {}", search_field); - let config_name_elements = document() - .get_elements_by_name(format!("{}-1", &search_field).as_str()); - let config_value_elements = document() - .get_elements_by_name(format!("{}-2", &search_field).as_str()); - for i in 0..config_name_elements.length() { - let item_one = config_name_elements.item(i).expect("missing span"); - let item_two = config_value_elements.item(i).expect("missing span"); - - let (config_name_element, config_value_element) = ( - item_one.dyn_ref::<HtmlSpanElement>().unwrap(), - item_two.dyn_ref::<HtmlSpanElement>().unwrap(), - ); - let (name, value) = ( - config_name_element - .inner_html() - .replace("<s>", "") - .replace("</s>", ""), - config_value_element - .inner_html() - .replace("<s>", "") - .replace("</s>", ""), - ); - - logging::log!("config name after replace {} and value {}", name, value); - config_name_element.set_inner_html(format!("{}", name).as_str()); - config_value_element.set_inner_html(format!("{}", value).as_str()); + // if metadata field is found, unstrike only that override + if let Some(metadata) = config.remove("metadata") { + for applied in metadata.as_array().unwrap_or(&vec![]).iter() { + logging::log!("applied config {:#?}", applied); + applied["override"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .for_each(|override_id| { + logging::log!("unstrike {:#?}", override_id); + unstrike(&override_id.as_str().unwrap().to_string(), &config) + }); } + } else { + unstrike(&String::from(""), &config); } }); }; @@ -257,6 +302,8 @@ pub fn home() -> impl IntoView { <ContextForm dimensions=dimension.to_owned().unwrap_or(vec![]) context=vec![] + is_standalone=true + handle_change=|_| () /> <div class="card-actions justify-end"> <button class="btn btn-primary" on:click=resolve_click> @@ -279,26 +326,40 @@ pub fn home() -> impl IntoView { match result { Some(Ok(config)) => { let rows = |k: &String, v: &Value| { - let key = k.replace("\"", "").trim().to_string(); - let value = format!("{}", v) - .replace("\"", "") - .trim() - .to_string(); - let name_identifier = gen_name_id(&key, &value); - view! { - <tr> - <td> - <span name={format!("{}-1", &name_identifier)} class="config-name"> - {key} - </span> - </td> - <td> - <span name={format!("{}-2", &name_identifier)} class="config-value"> - {value} - </span> - </td> - </tr> + let mut view_vector = vec![]; + println!("{:?}", v); + let default_iter = vec![(k.clone(), v.clone())]; + for (key, value) in v + .as_object() + .unwrap_or(&Map::from_iter(default_iter)) + .iter() + { + let key = key.replace("\"", "").trim().to_string(); + let value = format!("{}", value) + .replace("\"", "") + .trim() + .to_string(); + let unique_name = gen_name_id(k, &key, &value); + view_vector + .push( + view! { + <tr> + <td> + <span name=format!("{unique_name}-1") class="config-name"> + {key} + </span> + </td> + <td> + <span name=format!("{unique_name}-2") class="config-value"> + {value} + </span> + </td> + </tr> + } + .into_view(), + ) } + view_vector }; let contexts_views: Vec<_> = config .contexts @@ -308,8 +369,10 @@ pub fn home() -> impl IntoView { let rows: Vec<_> = context .override_with_keys .iter() - .filter_map(|key| config.overrides.get(key)) - .flat_map(|ovr| ovr.as_object().unwrap().iter()) + .filter_map(|key| { + let o = config.overrides.get(key); + if o.is_some() { Some((key, o.unwrap())) } else { None } + }) .map(|(k, v)| { rows(&k, &v) }) .collect(); view! { From 83d0f03fb652a0099b32e3c592c724b05f09bd6d Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Thu, 14 Dec 2023 19:10:33 +0530 Subject: [PATCH 214/352] feat: resolve page with unified UI --- crates/frontend/src/app.rs | 102 +++--- .../components/context_form/context_form.rs | 8 +- .../experiment_form/experiment_form.rs | 2 +- .../components/override_form/override_form.rs | 12 +- crates/frontend/src/components/stat/stat.rs | 2 +- .../pages/ContextOverride/ContextOverride.rs | 67 +--- .../src/pages/DefaultConfig/DefaultConfig.rs | 83 +++-- .../src/pages/Dimensions/Dimensions.rs | 16 +- crates/frontend/src/pages/Experiment/mod.rs | 4 +- .../pages/ExperimentList/ExperimentList.rs | 113 +++--- crates/frontend/src/pages/Home/Home.rs | 339 ++++++++++++------ 11 files changed, 428 insertions(+), 320 deletions(-) diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index fc087d131..380606670 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -18,56 +18,58 @@ pub fn App() -> impl IntoView { provide_context(tenant_rs); provide_context(tenant_ws); view! { - <Stylesheet id="leptos" href="/pkg/style.css"/> - // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> - <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> - <Link - href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" - rel="stylesheet" - /> - // sets the document title - <Title text="Welcome to Context Aware Config"/> - // content for this welcome page - <Router> - <body class="m-0 min-h-screen bg-gray-50 font-mono"> - <Layout> - <Routes> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/dimensions" - view=Dimensions - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/experiments" - view=ExperimentList - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/experiments/:id" - view=ExperimentPage - /> - <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/default-config" - view=DefaultConfig - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/overrides" - view=ContextOverride - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/resolve" - view=Home - /> - <Route path="/*any" view=NotFound/> + <html data-theme="light"> + <Stylesheet id="leptos" href="/pkg/style.css"/> + // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> + <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> + <Link + href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" + rel="stylesheet" + /> + // sets the document title + <Title text="Welcome to Context Aware Config"/> + // content for this welcome page + <Router> + <body class="m-0 min-h-screen bg-gray-50 font-mono"> + <Layout> + <Routes> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/dimensions" + view=Dimensions + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/experiments" + view=ExperimentList + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/experiments/:id" + view=ExperimentPage + /> + <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/default-config" + view=DefaultConfig + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="/admin/:tenant/overrides" + view=ContextOverride + /> + <Route + ssr=SsrMode::PartiallyBlocked + path="admin/:tenant/resolve" + view=Home + /> + <Route path="/*any" view=NotFound/> - </Routes> - </Layout> - </body> - </Router> + </Routes> + </Layout> + </body> + </Router> + </html> } } diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 728be09e3..ad193f375 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -213,11 +213,11 @@ where </div> </div> - <Show - when=move || is_standalone - > + <Show when=move || is_standalone> <div class="flex justify-end"> - <button class="btn" on:click:undelegated=on_click>Save</button> + <button class="btn" on:click:undelegated=on_click> + Save + </button> </div> </Show> </div> diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 3ca3be964..78e120c62 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -205,7 +205,7 @@ where </div> <div class="flex justify-end"> - <Button text="Submit".to_string() on_click = on_submit /> + <Button text="Submit".to_string() on_click=on_submit/> </div> </div> } diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 4ad75187d..88ec35435 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -95,9 +95,7 @@ where </div> </div> </div> - <Show - when=move || overrides.get().len() == 0 - > + <Show when=move || overrides.get().len() == 0> <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> <div> <i class="ri-windy-line text-3xl"></i> @@ -165,11 +163,11 @@ where /> </div> - <Show - when=move || is_standalone - > + <Show when=move || is_standalone> <div class="flex justify-end"> - <button class="btn" on:click:undelegated=on_submit>Save</button> + <button class="btn" on:click:undelegated=on_submit> + Save + </button> </div> </Show> </div> diff --git a/crates/frontend/src/components/stat/stat.rs b/crates/frontend/src/components/stat/stat.rs index e2ad5b152..484063195 100644 --- a/crates/frontend/src/components/stat/stat.rs +++ b/crates/frontend/src/components/stat/stat.rs @@ -11,7 +11,7 @@ pub fn Stat( <div class="stats shadow"> <div class="stat"> <div class="stat-figure text-purple-700"> - <i class={icon_class}></i> + <i class=icon_class></i> </div> <div class="stat-title">{heading}</div> <div class="stat-value">{number}</div> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 2c6b96bbb..12b9294f4 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -237,7 +237,7 @@ pub fn extract_and_format(condition: &Value) -> String { formatted_conditions.push(format_condition(cond)); } - formatted_conditions.join(" and ") + formatted_conditions.join(" && ") } else { // Handling single conditions format_condition(condition) @@ -348,9 +348,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <i class="ri-close-fill"></i> </button> </form> - <form - class="form-control w-full mt-8 bg-white text-gray-700 font-mono" - > + <form class="form-control w-full mt-8 bg-white text-gray-700 font-mono"> <div> <ContextModalForm handle_change=handle_context_change/> </div> @@ -358,7 +356,10 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <OverrideModalForm handle_change=handle_overrides_change/> </div> <div class="form-control mt-7"> - <Button text="Submit".to_string() on_click = move |ev:MouseEvent| on_submit(ev) /> + <Button + text="Submit".to_string() + on_click=move |ev: MouseEvent| on_submit(ev) + /> </div> <div class="mt-7"> <p class="text-red-500">{move || error_message.get()}</p> @@ -374,7 +375,7 @@ fn parse_conditions(input: String) -> Vec<(String, String, String)> { let operators = vec!["==", "in"]; // Split the string by "and" and iterate over each condition - for condition in input.split("and") { + for condition in input.split("&&") { let mut parts = Vec::new(); let mut operator_found = ""; @@ -451,7 +452,10 @@ pub fn ContextOverride() -> impl IntoView { <div class="p-8"> <div class="flex justify-between"> <h2 class="card-title">Overrides</h2> - <Button text="Create Context Overrides".to_string() on_click= |_| modal_action("my_modal_5","open") /> + <Button + text="Create Context Overrides".to_string() + on_click=|_| modal_action("my_modal_5", "open") + /> </div> <div class="space-y-6"> <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> @@ -494,9 +498,6 @@ pub fn ContextOverride() -> impl IntoView { context_views .push( view! { - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6 shadow-md"> <div class="flex justify-between"> @@ -523,7 +524,8 @@ pub fn ContextOverride() -> impl IntoView { </span> } }) - .collect::<Vec<_>>()} + .collect_view()} + </div> <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> <i class="ri-edit-line text-blue-500"></i> @@ -550,54 +552,13 @@ pub fn ContextOverride() -> impl IntoView { Some(Err(error)) => { vec![ view! { - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // ctx.set(new_ctx); - <div class="text-red-500"> {"Failed to fetch config data: "} {error} </div> }, ] } - None => { - vec![ - view! { - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // ctx.set(new_ctx); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // ctx.set(new_ctx); - - // let mut new_ctx: Vec<(String, String, String)> = vec![]; - // new_ctx.extend(ctx_values.clone()); - - // ctx.set(new_ctx); - - <div>Loading....</div> - }, - ] - } + None => vec![view! { <div>Loading....</div> }], } }) }} diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index b36d26a8f..140b7bbf5 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -200,7 +200,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { /> </div> <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> + <Button text="Submit".to_string() on_click=|_| modal_action("my_modal_5", "close")/> </div> </form> } @@ -275,52 +275,51 @@ pub fn DefaultConfig() -> impl IntoView { view! { <div class="p-8"> <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> - <Suspense - fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - } - > - { - move || { - let default_config = default_config_resource.get().unwrap_or(vec![]); - let total_default_config_keys = default_config.len().to_string(); - let table_settings = TableSettings { - redirect_prefix: None - }; - - let table_rows = default_config - .into_iter() - .map(|config| { json!(config).as_object().unwrap().to_owned() }) - .collect::<Vec<Map<String, Value>>>(); + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> - view! { - <div class="pb-4"> - <Stat - heading="Config Keys" - icon="ri-tools-line" - number={total_default_config_keys} - /> - </div> - <div class="card rounded-lg w-full bg-base-100 shadow"> - <div class="card-body"> - <div class="flex justify-between"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> - "Default Config" - </h2> - <Button text="Create Default Config".to_string() on_click= |_| modal_action("my_modal_5","open") /> - </div> - <Table - table_style="font-mono".to_string() - rows=table_rows - key_column="id".to_string() - columns=table_columns.get() - settings=table_settings + {move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let total_default_config_keys = default_config.len().to_string(); + let table_settings = TableSettings { + redirect_prefix: None, + }; + let table_rows = default_config + .into_iter() + .map(|config| { json!(config).as_object().unwrap().to_owned() }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat + heading="Config Keys" + icon="ri-tools-line" + number=total_default_config_keys + /> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Default Config" + </h2> + <Button + text="Create Default Config".to_string() + on_click=|_| modal_action("my_modal_5", "open") /> </div> + <Table + table_style="font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + settings=table_settings + /> </div> - } + </div> } - } + }} + </Suspense> </div> } diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 8c99dffaf..630851646 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -238,7 +238,10 @@ fn FormComponent( /> </div> <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> + <Button + text="Submit".to_string() + on_click=|_| modal_action("my_modal_5", "close") + /> </div> </form> </div> @@ -294,11 +297,7 @@ pub fn Dimensions() -> impl IntoView { _ => "0".to_string(), }; view! { - <Stat - heading="Dimensions" - icon="ri-ruler-2-fill" - number={total_items} - /> + <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> } }} <ModalComponent @@ -311,7 +310,10 @@ pub fn Dimensions() -> impl IntoView { <div class="card-body"> <div class="flex justify-between mb-2"> <h2 class="card-title">Dimensions</h2> - <Button text="Create Dimension".to_string() on_click= |_| modal_action("my_modal_5","open") /> + <Button + text="Create Dimension".to_string() + on_click=|_| modal_action("my_modal_5", "open") + /> </div> <div> diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 88c25b064..c1334595c 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -419,12 +419,12 @@ fn add_dialogs( <h3 class="font-bold text-lg">Edit Experiment</h3> // <ExperimentForm // name=experiment_rs.get().name - <div class="modal-action">// context=vec![] + // context=vec![] // variants=vec![] // dimensions=dimensions.get().unwrap_or(vec![]) // default_config=default_config.get().unwrap_or(vec![]) // /> - </div> + <div class="modal-action"></div> </div> </dialog> } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index f34534f6a..d15b961b5 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -122,23 +122,22 @@ pub fn ExperimentList() -> impl IntoView { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - { - move || { - let value = combined_resource.get(); - let total_items = match value { - Some(v) => v.experiments.total_items.to_string(), - _ => "0".to_string(), - }; - view! { - <Stat - heading="Experiments" - icon="ri-test-tube-fill" - number={total_items} - /> - } + {move || { + let value = combined_resource.get(); + let total_items = match value { + Some(v) => v.experiments.total_items.to_string(), + _ => "0".to_string(), + }; + view! { + <Stat + heading="Experiments" + icon="ri-test-tube-fill" + number=total_items + /> } - } + }} + </div> <div class="card rounded-xl w-full bg-base-100 shadow"> <div class="card-body"> @@ -150,16 +149,24 @@ pub fn ExperimentList() -> impl IntoView { on:click=move |event: web_sys::MouseEvent| { event.prevent_default(); set_open_form_modal.set(true); - if let Some(element) = document().get_element_by_id("create_exp_modal") { + if let Some(element) = document() + .get_element_by_id("create_exp_modal") + { log!("opening the experiment modal"); - let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); + let dialog_ele = element + .dyn_ref::<web_sys::HtmlDialogElement>(); match dialog_ele { - Some(ele) => { ele.show_modal(); }, - None => { log!("no modal element"); } + Some(ele) => { + ele.show_modal(); + } + None => { + log!("no modal element"); + } } } } > + Create Experiment <i class="ri-edit-2-line ml-2"></i> </button> @@ -243,52 +250,58 @@ pub fn ExperimentList() -> impl IntoView { </div> {move || { - let dim = combined_resource.get().unwrap_or(CombinedResource { - experiments: - ExperimentsResponse { + let dim = combined_resource + .get() + .unwrap_or(CombinedResource { + experiments: ExperimentsResponse { total_items: 0, total_pages: 0, data: vec![], - } - , - dimensions: vec![], - default_config: vec![], - }).dimensions; - let def_conf = combined_resource.get().unwrap_or(CombinedResource { - experiments: - ExperimentsResponse { + }, + dimensions: vec![], + default_config: vec![], + }) + .dimensions; + let def_conf = combined_resource + .get() + .unwrap_or(CombinedResource { + experiments: ExperimentsResponse { total_items: 0, total_pages: 0, data: vec![], - } - , - dimensions: vec![], - default_config: vec![], - }).default_config; + }, + dimensions: vec![], + default_config: vec![], + }) + .default_config; view! { - <Show - when=move || { open_form_modal.get() } - > + <Show when=move || { open_form_modal.get() }> <dialog id="create_exp_modal" class="modal"> <div class="modal-box w-12/12 max-w-5xl"> <div class="flex justify-between"> <h3 class="font-bold text-lg">Create Experiment</h3> <div> - <button - on:click=move |_| { - set_open_form_modal.set(false); - if let Some(element) = document().get_element_by_id("create_exp_modal") { - log!("FOUND AND CLOSING THE FORM"); - let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); - match dialog_ele { - Some(ele) => { ele.close(); }, - None => { log!("no modal element"); } + <button on:click=move |_| { + set_open_form_modal.set(false); + if let Some(element) = document() + .get_element_by_id("create_exp_modal") + { + log!("FOUND AND CLOSING THE FORM"); + let dialog_ele = element + .dyn_ref::<web_sys::HtmlDialogElement>(); + match dialog_ele { + Some(ele) => { + ele.close(); + } + None => { + log!("no modal element"); } - } else { - log!("outer close button no modal element"); } + } else { + log!("outer close button no modal element"); } - > + }> + <i class="ri-close-fill"></i> </button> </div> diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 4e459423f..57800c480 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -4,7 +4,10 @@ use serde_json::{Map, Value}; use wasm_bindgen::JsCast; use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; -use crate::{api::fetch_dimensions, components::context_form::context_form::ContextForm}; +use crate::{ + api::fetch_dimensions, + components::{context_form::context_form::ContextForm, Button::Button::Button}, +}; #[derive(Deserialize, Serialize, Clone)] pub struct Config { @@ -34,9 +37,14 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); - let url = - format!("http://localhost:8080/config/resolve?{context}&show_reasoning=true"); - match client.get(url).header("x-tenant", tenant).send().await { + let url = format!("http://localhost:8080/config/resolve?{context}"); + match client + .get(url) + .query(&[("show_reasoning", "true")]) + .header("x-tenant", tenant) + .send() + .await + { Ok(response) => { let config = response.json().await.map_err(|e| e.to_string())?; Ok(config) @@ -45,6 +53,52 @@ async fn resolve_config(tenant: String, context: String) -> Result<Value, String } } +fn parse_conditions(input: String) -> Vec<(String, String, String)> { + let mut conditions = Vec::new(); + let operators = vec!["==", "in"]; + + // Split the string by "&&" and iterate over each condition + for condition in input.split("&&") { + let mut parts = Vec::new(); + let mut operator_found = ""; + + // Check for each operator + for operator in &operators { + if condition.contains(operator) { + operator_found = operator; + parts = condition.split(operator).map(|s| s.trim()).collect(); + + // TODO: add this when context update is enabled + if parts.len() == 2 && operator == &"in" { + parts.swap(0, 1); + } + + break; + } + } + + if parts.len() == 2 { + let mut key = parts[0].to_string(); + let mut op = operator_found.to_string(); + let mut val = parts[1].to_string(); + // Add a space after key + key.push(' '); + if op == "==".to_string() { + val = val.trim_matches('"').to_string(); + op = "is".to_string(); + } else { + val = val.trim_matches('"').to_string(); + op = "has".to_string(); + } + op.push(' '); + + conditions.push((key, op, val)); + } + } + + conditions +} + pub fn extract_and_format(condition: &Value) -> String { if condition.is_object() && condition.get("and").is_some() { // Handling complex "and" conditions @@ -59,7 +113,7 @@ pub fn extract_and_format(condition: &Value) -> String { formatted_conditions.push(format_condition(cond)); } - formatted_conditions.join(" and ") + formatted_conditions.join(" && ") } else { // Handling single conditions format_condition(condition) @@ -118,6 +172,7 @@ fn gen_name_id(s0: &String, s1: &String, s2: &String) -> String { #[component] pub fn home() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + // let (config_display_rs, config_display_ws) = create_signal(Map::new()); let config_data = create_blocking_resource( move || tenant_rs.get(), move |tenant| fetch_config(tenant), @@ -162,22 +217,17 @@ pub fn home() -> impl IntoView { item_one.dyn_ref::<HtmlSpanElement>().unwrap(), item_two.dyn_ref::<HtmlSpanElement>().unwrap(), ); - let (name, value) = ( - config_name_element - .inner_html() - .replace("<span class=\"text-green-600\">", "") - .replace("<span class=\"text-orange-600\">", "") - .replace("</span>", ""), - config_value_element - .inner_html() - .replace("<span class=\"text-green-600\">", "") - .replace("<span class=\"text-orange-600\">", "") - .replace("</span>", ""), + let _ = config_name_element.class_list().add_2("text-black", "font-bold"); + let _ = config_name_element.class_list().remove_1("text-gray-600"); + let _ = config_value_element.class_list().add_2("text-black", "font-bold"); + let _ = config_value_element + .class_list() + .remove_1("text-gray-600"); + logging::log!( + "config name after replace {} and value {}", + config_name_element.to_string(), + config_value_element.to_string() ); - - logging::log!("config name after replace {} and value {}", name, value); - config_name_element.set_inner_html(format!("<span class=\"text-green-600\">{}</span>", name).as_str()); - config_value_element.set_inner_html(format!("<span class=\"text-green-600\">{}</span>", value).as_str()); } } }; @@ -234,28 +284,10 @@ pub fn home() -> impl IntoView { config_name_elements.item(i).unwrap(), config_value_elements.item(i).unwrap(), ); - config_name_element.set_inner_html( - format!( - "<span class=\"text-orange-600\">{}</span>", - config_name_element - .inner_html() - .replace("<span class=\"text-green-600\">", "") - .replace("<span class=\"text-orange-600\">", "") - .replace("</span>", "") - ) - .as_str(), - ); - config_value_element.set_inner_html( - format!( - "<span class=\"text-orange-600\">{}</span>", - config_value_element - .inner_html() - .replace("<span class=\"text-green-600\">", "") - .replace("<span class=\"text-orange-600\">", "") - .replace("</span>", ""), - ) - .as_str(), - ); + let _ = config_name_element.class_list().remove_2("text-black", "font-bold"); + let _ = config_name_element.class_list().add_1("text-gray-600"); + let _ = config_value_element.class_list().remove_2("text-black", "font-bold"); + let _ = config_value_element.class_list().add_1("text-gray-600"); } logging::log!("query vector {:#?}", query_vector); // resolve the context and get the config that would apply @@ -269,54 +301,146 @@ pub fn home() -> impl IntoView { logging::log!("resolved config {:#?}", config); // unstrike those that we want to show the user // if metadata field is found, unstrike only that override - if let Some(metadata) = config.remove("metadata") { - for applied in metadata.as_array().unwrap_or(&vec![]).iter() { - logging::log!("applied config {:#?}", applied); - applied["override"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .for_each(|override_id| { - logging::log!("unstrike {:#?}", override_id); - unstrike(&override_id.as_str().unwrap().to_string(), &config) - }); + match config.remove("metadata") { + Some(Value::Array(metadata)) => { + if metadata.len() == 0 { + logging::log!("unstrike default config"); + unstrike(&String::new(), &config); + } + for applied in metadata.iter() { + logging::log!("applied config {:#?}", applied); + applied["override"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .for_each(|override_id| { + logging::log!("unstrike {:#?}", override_id); + unstrike( + &override_id.as_str().unwrap().to_string(), + &config, + ) + }); + } + } + _ => { + logging::log!( + "no metadata recieved, default config is the config to be used" + ); } - } else { - unstrike(&String::from(""), &config); } + logging::log!("unstrike default config if needed"); + unstrike(&String::new(), &config); + + let resolution_card = document() + .get_element_by_id("resolved_table_body") + .expect("resolve table card not found"); + + let mut table_rows = String::new(); + for (key, value) in config.iter() { + table_rows.push_str( + format!( + "<tr><td>{key}</td><td>{}</td></tr>", + value.as_str().unwrap() + ) + .as_str(), + ) + } + resolution_card.set_inner_html(&table_rows); }); }; view! { <div class="flex w-full flex-row mt-5 justify-evenly"> - <Suspense fallback=move || { - view! { <p>"Loading..."</p> } - }> - {move || { - dimension_resource - .with(|dimension| { - view! { - <div class="card m-10 bg-base w-4/12"> - <div class="card-body"> - <h2 class="card-title">Resolve Configs</h2> - - <ContextForm - dimensions=dimension.to_owned().unwrap_or(vec![]) - context=vec![] - is_standalone=true - handle_change=|_| () - /> - <div class="card-actions justify-end"> - <button class="btn btn-primary" on:click=resolve_click> - Resolve - </button> + <div class="card mr-5 ml-5 mt-6 h-4/5 shadow bg-base-100 w-4/12"> + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + {move || { + dimension_resource + .with(|dimension| { + view! { + <div class="card m-2 bg-base-100"> + <div class="card-body"> + <h2 class="card-title">Resolve Configs</h2> + + <ContextForm + dimensions=dimension.to_owned().unwrap_or(vec![]) + context=vec![] + is_standalone=false + handle_change=|_| () + /> + <div class="card-actions justify-end"> + <div class="form-control"> + <label class="cursor-pointer label"> + <span class="label-text ml-1">Display All Configs</span> + <input type="checkbox" class="toggle bg-purple-600" checked /> + </label> + </div> + <Button text="Resolve".to_string() on_click=resolve_click/> + </div> </div> </div> - </div> + } + }) + }} + + </Suspense> + // config suspense + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + + {config_data + .with(move |conf| { + match conf { + Some(Ok(config)) => { + let default_configs = config.default_configs.clone(); + view! { + <div class="card m-2 bg-base-100"> + <div class="card-body"> + <h2 class="card-title">Resolved Config</h2> + <table class="table"> + <thead> + <tr> + <th>Config Key</th> + <th>Value</th> + </tr> + </thead> + <tbody id="resolved_table_body"> + <For + each=move || { default_configs.clone().into_iter() } + + key=|(key, value)| format!("{key}-{value}") + children=move |(config, value)| { + view! { + <tr> + <td>{config}</td> + <td>{value.as_str().unwrap().to_string()}</td> + </tr> + } + } + /> + + </tbody> + </table> + </div> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="error"> + {"Failed to fetch config data: "} {error} + </div> + } + } + None => { + view! { <div class="error">{"No config data fetched"}</div> } + } } - }) - }} + })} - </Suspense> + </Suspense> + </div> <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } }> @@ -325,7 +449,7 @@ pub fn home() -> impl IntoView { .with(move |result| { match result { Some(Ok(config)) => { - let rows = |k: &String, v: &Value| { + let rows = |k: &String, v: &Value, striked: bool| { let mut view_vector = vec![]; println!("{:?}", v); let default_iter = vec![(k.clone(), v.clone())]; @@ -343,20 +467,14 @@ pub fn home() -> impl IntoView { view_vector .push( view! { - <tr> - <td> - <span name=format!("{unique_name}-1") class="config-name"> - {key} - </span> - </td> - <td> - <span name=format!("{unique_name}-2") class="config-value"> - {value} - </span> - </td> - </tr> - } - .into_view(), + < tr > < td > < span name = format!("{unique_name}-1") class + = "config-name" class : text-black = { ! striked } class : font-bold = { !striked } + class : text - gray - 600 = { striked } > { key } </ span + > </ td > < td > < span name = format!("{unique_name}-2") + class = "config-value" class : text-black = { ! + striked } class : font-bold = { !striked } class : text - gray - 600 = { striked } > { + value } </ span > </ td > </ tr > + }, ) } view_vector @@ -365,7 +483,7 @@ pub fn home() -> impl IntoView { .contexts .iter() .map(|context| { - let condition = extract_and_format(&context.condition); + let condition = parse_conditions(extract_and_format(&context.condition)); let rows: Vec<_> = context .override_with_keys .iter() @@ -373,16 +491,31 @@ pub fn home() -> impl IntoView { let o = config.overrides.get(key); if o.is_some() { Some((key, o.unwrap())) } else { None } }) - .map(|(k, v)| { rows(&k, &v) }) + .map(|(k, v)| { rows(&k, &v, true) }) .collect(); view! { <div class="card bg-base-100 shadow m-6"> <div class="card-body"> <h2 class="card-title"> - "Condition: " - <div class="badge badge-lg badge-primary p-5"> - {&condition} - </div> + {condition + .iter() + .map(|(dim, op, val)| { + view! { + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> + <span class="font-mono font-medium context_condition text-gray-500"> + {dim} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {op} + </span> + <span class="font-mono font-semibold context_condition"> + {val} + </span> + </span> + } + }) + .collect_view()} + </h2> <table class="table mt-10"> <thead> @@ -406,7 +539,7 @@ pub fn home() -> impl IntoView { let default_config: Vec<_> = config .default_configs .iter() - .map(|(k, v)| { rows(&k, &v) }) + .map(|(k, v)| { rows(&k, &v, false) }) .collect(); vec![ view! { @@ -448,4 +581,4 @@ pub fn home() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} From 8b0d0a39e506ac5b8e7e92f70c1452f1fa484974 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Mon, 18 Dec 2023 17:33:42 +0530 Subject: [PATCH 215/352] fix: resolve UI bugs --- crates/frontend/src/pages/Experiment/mod.rs | 82 ++++++++++--------- .../pages/ExperimentList/ExperimentList.rs | 1 - crates/frontend/src/pages/Home/Home.rs | 28 ++++--- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index c1334595c..6068d9dbb 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -167,23 +167,23 @@ fn experiment_detail_view( let (ctxs, _) = create_signal(contexts); view! { - <div class="flex flex-col overflow-x-auto p-2"> + <div class="flex flex-col overflow-x-auto p-2 bg-transparent"> {move || { experiment .with(|exp| { let class_name = match exp.status { ExperimentStatusType::CREATED => { - "badge ml-3 mb-1 badge-lg badge-primary" + "badge text-white ml-3 mb-1 badge-xl badge-info" } ExperimentStatusType::INPROGRESS => { - "badge ml-3 mb-1 badge-lg badge-warning" + "badge text-white ml-3 mb-1 badge-xl badge-warning" } ExperimentStatusType::CONCLUDED => { - "badge ml-3 mb-1 badge-lg badge-success" + "badge text-white ml-3 mb-1 badge-xl badge-success" } }; view! { - <h1 class="text-4xl pt-4 font-extrabold"> + <h1 class="text-2xl pt-4 font-extrabold"> {&exp.name} <span class=class_name>{exp.status.to_string()}</span> </h1> } @@ -198,14 +198,14 @@ fn experiment_detail_view( ExperimentStatusType::CREATED => { view! { <button - class="btn join-item" + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" onclick="edit_exp_modal.showModal()" > <i class="ri-edit-line"></i> Edit </button> <button - class="btn join-item" + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" value=&exp.id on:click=move |button_event| spawn_local(async move { let value = event_target_value(&button_event); @@ -222,14 +222,14 @@ fn experiment_detail_view( ExperimentStatusType::INPROGRESS => { view! { <button - class="btn join-item" + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" onclick="conclude_exp_modal.showModal()" > <i class="ri-stop-circle-line"></i> Conclude </button> <button - class="btn join-item" + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" onclick="ramp_exp_modal.showModal()" > <i class="ri-flight-takeoff-line"></i> @@ -256,41 +256,43 @@ fn experiment_detail_view( }) }} - </div> <div class="stats shadow-xl mt-5"> - <div class="stat"> + </div> + <div class="flex bg-base-100 flex-row gap-2 justify-between flex-wrap shadow m-5"> + <div class="stat w-2/12"> <div class="stat-title">Experiment ID</div> - <div class="stat-value">{experiment.get().id}</div> + <div class="stat-value text-sm">{experiment.get().id}</div> </div> - <div class="stat"> + <div class="stat w-2/12"> <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value">{move || experiment.get().traffic_percentage}</div> + <div class="stat-value text-sm">{move || experiment.get().traffic_percentage}</div> </div> - <div class="stat"> + <div class="stat w-2/12"> <div class="stat-title">Created by</div> - <div class="stat-value">{experiment.get().created_by}</div> + <div class="stat-value text-sm">{experiment.get().created_by}</div> </div> - <div class="stat"> + <div class="stat w-2/12"> <div class="stat-title">Created at</div> - <div class="stat-value"> + <div class="stat-value text-sm"> {format!("{}", experiment.get().created_at.format("%v"))} </div> </div> - <div class="stat"> + <div class="stat w-2/12"> <div class="stat-title">Last Modified</div> - <div class="stat-value"> + <div class="stat-value text-sm"> {move || { experiment.with(|exp| format!("{}", &exp.last_modified.format("%v"))) }} </div> </div> - </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + </div> + <div class="card bg-base-100 max-w-screen shadow m-5"> <div class="card-body"> <h2 class="card-title">Context</h2> - <div class="flex flex-row"> + <div class="flex flex-row flex-wrap gap-2"> {move || { let contexts = move || ctxs.get().into_iter(); - let mut view: Vec<_> = Vec::new(); + let mut view: Vec<HtmlElement<html::Div>> = Vec::new(); for item in contexts() { for (_, value) in item.as_object().unwrap().into_iter() { let rule_vector = value.as_array().unwrap(); @@ -302,11 +304,11 @@ fn experiment_detail_view( let dimension = var.as_object().unwrap().get("var").unwrap(); view.push( view! { - <div class="stat"> + <div class="stat w-3/12"> <div class="stat-title"> {format!("{}", dimension.as_str().unwrap())} </div> - <div class="stat-value"> + <div class="stat-value text-base"> {format!("{}", value.as_str().unwrap())} </div> </div> @@ -319,7 +321,7 @@ fn experiment_detail_view( </div> </div> - </div> <div class="card bg-base max-w-screen shadow-xl mt-5"> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> <div class="card-body"> <h2 class="card-title">Variants</h2> <div class="overflow-x-auto overflow-y-auto"> @@ -416,6 +418,9 @@ fn add_dialogs( ExperimentStatusType::CREATED => view! { <dialog id="edit_exp_modal" class="modal"> <div class="modal-box"> + <form method="dialog"> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + </form> <h3 class="font-bold text-lg">Edit Experiment</h3> // <ExperimentForm // name=experiment_rs.get().name @@ -432,6 +437,10 @@ fn add_dialogs( ExperimentStatusType::INPROGRESS => view! { <dialog id="conclude_exp_modal" class="modal"> <div class="modal-box"> + <form method="dialog"> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + </form> + <h3 class="font-bold text-lg">Conclude This Experiment</h3> <p class="py-4"> Choose a variant to conclude with, this variant becomes @@ -446,7 +455,7 @@ fn add_dialogs( VariantType::CONTROL => { view! { <button - class="btn btn-block btn-outline btn-success m-2" + class="btn btn-block btn-outline btn-info m-2" on:click=move |_| spawn_local(async move { let e = experiment_rs.get(); let variant = variant.get(); @@ -464,7 +473,7 @@ fn add_dialogs( VariantType::EXPERIMENTAL => { view! { <button - class="btn btn-block btn-outline btn-info m-2" + class="btn btn-block btn-outline btn-success m-2" on:click=move |_| spawn_local(async move { let e = experiment_rs.get(); let variant = variant.get(); @@ -486,15 +495,13 @@ fn add_dialogs( }} </form> - <div class="modal-action"> - <form method="dialog"> - <button class="btn">Close</button> - </form> - </div> </div> </dialog> <dialog id="ramp_exp_modal" class="modal"> <div class="modal-box"> + <form method="dialog"> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + </form> <h3 class="font-bold text-lg">Ramp up with release</h3> <p class="py-4">Increase the traffic being redirected to the variants</p> <form method="dialog" on:submit=on_submit> @@ -513,16 +520,11 @@ fn add_dialogs( } /> - <button class="btn btn-block btn-outline btn-success m-2">Set</button> + <button class="btn btn-block text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">Set</button> </form> - <div class="modal-action"> - <form method="dialog"> - <button class="btn">Close</button> - </form> - </div> </div> </dialog> }.into_view(), - ExperimentStatusType::CONCLUDED => view! { <h1>conclude</h1> }.into_view(), + ExperimentStatusType::CONCLUDED => view! {}.into_view(), } } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index d15b961b5..9d5e7dcd8 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -1,7 +1,6 @@ use futures::join; use leptos::logging::*; use leptos::*; -use leptos_dom::*; use chrono::{prelude::Utc, TimeZone}; use serde::{Deserialize, Serialize}; diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 57800c480..c85c3d48b 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -187,6 +187,8 @@ pub fn home() -> impl IntoView { }, ); + let (display_configs_rs, display_configs_ws) = create_signal(true); + let unstrike = |search_field_prefix: &String, config: &Map<String, Value>| { for (dimension, value) in config.into_iter() { let search_field_prefix = if search_field_prefix.is_empty() { @@ -218,11 +220,11 @@ pub fn home() -> impl IntoView { item_two.dyn_ref::<HtmlSpanElement>().unwrap(), ); let _ = config_name_element.class_list().add_2("text-black", "font-bold"); - let _ = config_name_element.class_list().remove_1("text-gray-600"); + let _ = config_name_element.class_list().remove_1("text-gray-300"); let _ = config_value_element.class_list().add_2("text-black", "font-bold"); let _ = config_value_element .class_list() - .remove_1("text-gray-600"); + .remove_1("text-gray-300"); logging::log!( "config name after replace {} and value {}", config_name_element.to_string(), @@ -285,9 +287,9 @@ pub fn home() -> impl IntoView { config_value_elements.item(i).unwrap(), ); let _ = config_name_element.class_list().remove_2("text-black", "font-bold"); - let _ = config_name_element.class_list().add_1("text-gray-600"); + let _ = config_name_element.class_list().add_1("text-gray-300"); let _ = config_value_element.class_list().remove_2("text-black", "font-bold"); - let _ = config_value_element.class_list().add_1("text-gray-600"); + let _ = config_value_element.class_list().add_1("text-gray-300"); } logging::log!("query vector {:#?}", query_vector); // resolve the context and get the config that would apply @@ -369,12 +371,6 @@ pub fn home() -> impl IntoView { handle_change=|_| () /> <div class="card-actions justify-end"> - <div class="form-control"> - <label class="cursor-pointer label"> - <span class="label-text ml-1">Display All Configs</span> - <input type="checkbox" class="toggle bg-purple-600" checked /> - </label> - </div> <Button text="Resolve".to_string() on_click=resolve_click/> </div> </div> @@ -469,10 +465,10 @@ pub fn home() -> impl IntoView { view! { < tr > < td > < span name = format!("{unique_name}-1") class = "config-name" class : text-black = { ! striked } class : font-bold = { !striked } - class : text - gray - 600 = { striked } > { key } </ span + class : text - gray - 300 = { striked } > { key } </ span > </ td > < td > < span name = format!("{unique_name}-2") class = "config-value" class : text-black = { ! - striked } class : font-bold = { !striked } class : text - gray - 600 = { striked } > { + striked } class : font-bold = { !striked } class : text - gray - 300 = { striked } > { value } </ span > </ td > </ tr > }, ) @@ -544,6 +540,14 @@ pub fn home() -> impl IntoView { vec![ view! { <div class="mb-4 w-8/12 overflow-y-auto max-h-screen"> + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> {new_context_views} <div class="card bg-base-100 shadow m-6"> <div class="card-body"> From 97f7f1bcfb6a0f4b24eb679cf6d94f1662f8fe18 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Mon, 18 Dec 2023 21:31:05 +0530 Subject: [PATCH 216/352] feat: added validation inside default config form , formatted dates , added disable feature of edit --- .../src/pages/DefaultConfig/DefaultConfig.rs | 319 ++++++++++++------ .../src/pages/Dimensions/Dimensions.rs | 222 ++++++++---- 2 files changed, 362 insertions(+), 179 deletions(-) diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 140b7bbf5..28b2f8890 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -24,6 +24,18 @@ pub struct RowData { pub value: String, } +// fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { +// // Parse the input string into a serde_json::Value +// let parsed = serde_json::from_str::<Value>(input) +// .expect("Failed to parse JSON"); + +// // Ensure the Value is an Array and convert it to Vec<Value> +// match parsed { +// Value::Array(arr) => arr, +// _ => panic!("Input is not a JSON array"), +// } +// } + pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); let host = "http://localhost:8080"; @@ -49,10 +61,37 @@ pub async fn create_default_config( let url = format!("{host}/default-config/{key}"); let mut req_body: HashMap<&str, Value> = HashMap::new(); let mut schema: Map<String, Value> = Map::new(); - schema.insert("type".to_string(), Value::String(key_type)); - schema.insert("pattern".to_string(), Value::String(pattern)); - req_body.insert("value", Value::String(value)); + match key_type.as_str() { + "number" => { + schema.insert("type".to_string(), Value::String(key_type.clone())); + req_body.insert( + "value", + Value::Number(value.clone().parse::<i32>().unwrap().into()), + ); + } + // "Enum" => { + // let array = parse_string_to_json_value_vec(pattern.clone().as_str()); + // schema.insert("type".to_string(),Value::String("string".to_string())); + // schema.insert("enum".to_string(), Value::Array(array)); + // req_body.insert("value", Value::Array(value2)); + // }, + "Pattern" | "Enum" => { + schema.insert("type".to_string(), Value::String("string".to_string())); + schema.insert("pattern".to_string(), Value::String(pattern.clone())); + } + _ => { + let json_pattern = serde_json::from_str::<Value>(&pattern.clone()) + .map_err(|e| e.to_string())?; + req_body.insert("schema", json_pattern); + } + } + + if key_type != "number".to_string() { + req_body.insert("value", Value::String(value)); + } + req_body.insert("schema", Value::Object(schema)); + let response = client .put(url) .header("x-tenant", tenant) @@ -83,17 +122,23 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { #[component] fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { use leptos::html::Input; + use leptos::html::Textarea; let handle_submit = handle_submit.clone(); let global_state = use_context::<RwSignal<RowData>>(); let row_data = global_state.unwrap().get(); - let (key, set_key) = create_signal(row_data.key); - let (value, set_value) = create_signal(row_data.value); - let (keytype, set_keytype) = create_signal("string".to_string()); - let (pattern, set_pattern) = create_signal(".*".to_string()); + let (key, set_key) = create_signal("".to_string()); + let (value, set_value) = create_signal("".to_string()); + let (keytype, set_keytype) = create_signal("".to_string()); + let (pattern, set_pattern) = create_signal("".to_string()); + + let edit_signal = use_context::<RwSignal<bool>>(); + + let (show_labels, set_show_labels) = create_signal(false); create_effect(move |_| { if let Some(row_data) = global_state { + logging::log!("default config create effect"); set_key.set(row_data.get().key.clone().to_string()); set_value.set(row_data.get().value.clone().to_string()); } @@ -101,8 +146,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let input_element: NodeRef<Input> = create_node_ref(); let input_element_two: NodeRef<Input> = create_node_ref(); - let input_element_three: NodeRef<Input> = create_node_ref(); - let input_element_four: NodeRef<Input> = create_node_ref(); + let input_element_three: NodeRef<Textarea> = create_node_ref(); let on_submit = { let handle_submit = handle_submit.clone(); @@ -112,12 +156,11 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let value1 = input_element.get().expect("<input> to exist").value(); let value2 = input_element_two.get().expect("<input> to exist").value(); let value3 = input_element_three.get().expect("<input> to exist").value(); - let value4 = input_element_four.get().expect("<input> to exist").value(); set_key.set(value1.clone()); set_value.set(value2.clone()); - set_keytype.set(value3.clone()); - set_pattern.set(value4.clone()); + set_pattern.set(value3.clone()); + let handle_submit_clone = handle_submit.clone(); spawn_local({ @@ -148,7 +191,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { <form - class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" + class="form-control w-full overflow-auto space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit > <div class="form-control"> @@ -156,6 +199,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { <span class="label-text text-gray-700 font-mono">Key</span> </label> <input + disabled = move || {edit_signal.unwrap().get()} type="text" placeholder="Key" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -163,66 +207,119 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { node_ref=input_element /> </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> - </label> - <input - type="text" - placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=value - node_ref=input_element_two - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Type</span> - </label> - <input - type="text" - placeholder="Type" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=keytype - node_ref=input_element_three - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> - </label> - <input - type="text" - placeholder="Pattern" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=pattern - node_ref=input_element_four - /> + + + <div tabindex = "0" class="dropdown dropdown-bottom"> + <div tabindex="0" role="button" class="btn m-1"> + <i class="ri-arrow-down-s-line"></i> + Add Schema </div> - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click=|_| modal_action("my_modal_5", "close")/> + + <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 + rounded-box w-52"> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("number".to_string()); + }> + <a>"Number"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + }> + <a>"String (Enum)"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + }> + <a>"String (Regex)"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Other".to_string()); + set_pattern.set("".to_string()); + }> + <a>"Other"</a> + </li> + </ul> </div> - </form> + { + move || { + view!{ + <Show when = move || (keytype.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="number" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=value + node_ref=input_element_two + /> + </div> + </Show> + + <Show when = move || (show_labels.get() && (keytype.get() != "number")) > + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="text" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=value + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">{keytype.get()}</span> + </label> + <textarea + type = "text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + + node_ref=input_element_three + > {pattern.get()} + </textarea> + + </div> + </Show> + } + } + } + <div class="form-control mt-6"> + <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> + </div> + </form> } } -fn custom_formatter(value: &str, row: &Map<String, Value>) -> View { - let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); +fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { + let global_signal = use_context::<RwSignal<RowData>>().unwrap(); let row_key = row["key"].clone().to_string().replace("\"", ""); let row_value = row["value"].clone().to_string().replace("\"", ""); + let edit_signal = use_context::<RwSignal<bool>>().unwrap(); let edit_click_handler = move |_| { let row_data = RowData { key: row_key.clone(), value: row_value.clone(), }; - intermediate_signal.set(Some(row_data)); + edit_signal.set(true); + global_signal.set(row_data); js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); }; - let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; - view! { <span>{edit_icon}</span> }.into_view() + view! { <span on:click = edit_click_handler>{edit_icon}</span> }.into_view() } #[component] @@ -231,16 +328,8 @@ pub fn DefaultConfig() -> impl IntoView { let global_state = create_rw_signal(RowData::default()); provide_context(global_state); - let intermediate_signal = create_rw_signal(None::<RowData>); - - // Listener for intermediate signal - create_effect(move |_| { - if let Some(row_data) = intermediate_signal.get() { - global_state.set(row_data.clone()); - } - }); - - provide_context(intermediate_signal.clone()); + let edit_signal = create_rw_signal(true); + provide_context(edit_signal); let query = use_query_map(); @@ -275,52 +364,66 @@ pub fn DefaultConfig() -> impl IntoView { view! { <div class="p-8"> <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> - <Suspense fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - }> - - {move || { - let default_config = default_config_resource.get().unwrap_or(vec![]); - let total_default_config_keys = default_config.len().to_string(); - let table_settings = TableSettings { - redirect_prefix: None, - }; - let table_rows = default_config - .into_iter() - .map(|config| { json!(config).as_object().unwrap().to_owned() }) - .collect::<Vec<Map<String, Value>>>(); - view! { - <div class="pb-4"> - <Stat - heading="Config Keys" - icon="ri-tools-line" - number=total_default_config_keys - /> - </div> - <div class="card rounded-lg w-full bg-base-100 shadow"> - <div class="card-body"> - <div class="flex justify-between"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> - "Default Config" - </h2> - <Button - text="Create Default Config".to_string() - on_click=|_| modal_action("my_modal_5", "open") + <Suspense + fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + } + > + { + + let edit_signal = edit_signal.clone(); + move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let total_default_config_keys = default_config.len().to_string(); + let table_settings = TableSettings { + redirect_prefix: None + }; + + let edit_signal = edit_signal.clone(); + + let table_rows = default_config + .into_iter() + .map(|config| { + let mut ele_map = json!(config).as_object().unwrap().to_owned(); + ele_map.insert("created_at".to_string(),json!(config.created_at.format("%v").to_string())); + ele_map + }) + .collect::<Vec<Map<String, Value>>>(); + + view! { + <div class="pb-4"> + <Stat + heading="Config Keys" + icon="ri-tools-line" + number={total_default_config_keys} + /> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Default Config" + </h2> + <Button text="Create Key".to_string() on_click= { + let edit_signal_clone = edit_signal.to_owned(); + move |_| { + edit_signal_clone.set(false); + modal_action("my_modal_5","open"); + }}/> + </div> + <Table + table_style="font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + settings=table_settings /> </div> - <Table - table_style="font-mono".to_string() - rows=table_rows - key_column="id".to_string() - columns=table_columns.get() - settings=table_settings - /> </div> - </div> + } } - }} - + } </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 630851646..474ba4787 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -25,13 +25,25 @@ pub struct RowData { pub pattern: String, } +fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { + // Parse the input string into a serde_json::Value + let parsed = serde_json::from_str::<Value>(input).expect("Failed to parse JSON"); + + // Ensure the Value is an Array and convert it to Vec<Value> + match parsed { + Value::Array(arr) => arr, + _ => panic!("Input is not a JSON array"), + } +} + pub fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { - let intermediate_signal = use_context::<RwSignal<Option<RowData>>>().unwrap(); + let global_signal = use_context::<RwSignal<RowData>>().unwrap(); let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); let row_priority = row["priority"].clone().to_string().replace("\"", ""); let schema = row["schema"].clone().to_string(); let schema_object = serde_json::from_str::<serde_json::Value>(&schema).unwrap(); + let edit_signal = use_context::<RwSignal<bool>>().unwrap(); let row_type = schema_object.get("type").unwrap().to_string(); let row_pattern = schema_object @@ -46,7 +58,8 @@ pub fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { type_: row_type.clone(), pattern: row_pattern.clone(), }; - intermediate_signal.set(Some(row_data)); + global_signal.set(row_data); + edit_signal.set(true); js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); }; @@ -70,14 +83,25 @@ pub async fn create_dimension( let mut req_body: HashMap<&str, Value> = HashMap::new(); let mut schema: Map<String, Value> = Map::new(); - schema.insert( - "type".to_string(), - Value::String(key_type.replace("\"", "")), - ); - schema.insert( - "pattern".to_string(), - Value::String(pattern.replace("\"", "")), - ); + match key_type.as_str() { + "number" => { + schema.insert("type".to_string(), Value::String(key_type.clone())); + } + "Enum" => { + let array = parse_string_to_json_value_vec(pattern.clone().as_str()); + schema.insert("type".to_string(), Value::String("string".to_string())); + schema.insert("enum".to_string(), Value::Array(array)); + } + "Pattern" => { + schema.insert("type".to_string(), Value::String("string".to_string())); + schema.insert("pattern".to_string(), Value::String(pattern.clone())); + } + _ => { + let json_pattern = serde_json::from_str::<Value>(&pattern.clone()) + .map_err(|e| e.to_string())?; + req_body.insert("schema", json_pattern); + } + } req_body.insert("dimension", Value::String(key)); req_body.insert("priority", Value::Number(priority.into())); @@ -112,6 +136,7 @@ fn FormComponent( tenant: ReadSignal<String>, ) -> impl IntoView { use leptos::html::Input; + use leptos::html::Textarea; let handle_submit = handle_submit.clone(); let global_state = use_context::<RwSignal<RowData>>(); let row_data = global_state.unwrap().get(); @@ -120,9 +145,12 @@ fn FormComponent( let (priority, set_priority) = create_signal(row_data.priority); let (keytype, set_keytype) = create_signal(row_data.type_); let (pattern, set_pattern) = create_signal(row_data.pattern); + let (show_labels, set_show_labels) = create_signal(false); + let edit_signal = use_context::<RwSignal<bool>>(); create_effect(move |_| { if let Some(row_data) = global_state { + logging::log!("default config create effect"); set_dimension.set(row_data.get().dimension.clone().to_string()); set_priority.set(row_data.get().priority.clone()); set_keytype.set(row_data.get().type_.clone().to_string()); @@ -132,8 +160,7 @@ fn FormComponent( let input_element: NodeRef<Input> = create_node_ref(); let input_element_two: NodeRef<Input> = create_node_ref(); - let input_element_three: NodeRef<Input> = create_node_ref(); - let input_element_four: NodeRef<Input> = create_node_ref(); + let input_element_three: NodeRef<Textarea> = create_node_ref(); let on_submit = { let handle_submit = handle_submit.clone(); @@ -143,12 +170,10 @@ fn FormComponent( let value1 = input_element.get().expect("<input> to exist").value(); let value2 = input_element_two.get().expect("<input> to exist").value(); let value3 = input_element_three.get().expect("<input> to exist").value(); - let value4 = input_element_four.get().expect("<input> to exist").value(); set_dimension.set(value1.clone()); set_priority.set(value2.clone()); - set_keytype.set(value3.clone()); - set_pattern.set(value4.clone()); + set_pattern.set(value3.clone()); let handle_submit_clone = handle_submit.clone(); spawn_local({ @@ -194,6 +219,7 @@ fn FormComponent( <span class="label-text text-gray-700 font-mono">Dimension</span> </label> <input + disabled = move || {edit_signal.unwrap().get()} type="text" placeholder="Dimension" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -201,47 +227,92 @@ fn FormComponent( node_ref=input_element /> </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Type</span> - </label> - <input - type="text" - placeholder="Type" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=keytype - node_ref=input_element_three - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Pattern (regex)</span> - </label> - <input - type="text" - placeholder="Pattern" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=pattern - node_ref=input_element_four - /> + <div tabindex = "0" class="dropdown dropdown-bottom"> + <div tabindex="0" role="button" class="btn m-1"> + <i class="ri-arrow-down-s-line"></i> + Add Schema + </div> + + <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 + rounded-box w-52"> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("number".to_string()); + }> + <a>"Number"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + }> + <a>"String (Enum)"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + }> + <a>"String (Regex)"</a> + </li> + <li on:click = move |_| { + set_show_labels.set(true); + set_keytype.set("Other".to_string()); + }> + <a>"Other"</a> + </li> + </ul> </div> + { + move || { + view!{ + <Show when = move || (keytype.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + </Show> + + <Show when = move || (show_labels.get() && (keytype.get() != "number")) > + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">{keytype.get()}</span> + </label> + <textarea + type = "text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + + node_ref=input_element_three + > {pattern.get()} + </textarea> + + </div> + </Show> + } + } + } <div class="form-control mt-6"> - <Button - text="Submit".to_string() - on_click=|_| modal_action("my_modal_5", "close") - /> + <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> </div> </form> </div> @@ -255,15 +326,9 @@ pub fn Dimensions() -> impl IntoView { let global_state = create_rw_signal(RowData::default()); provide_context(global_state); - let intermediate_signal = create_rw_signal(None::<RowData>); - - create_effect(move |_| { - if let Some(row_data) = intermediate_signal.get() { - global_state.set(row_data.clone()); - } - }); + let edit_signal = create_rw_signal(true); + provide_context(edit_signal); - provide_context(intermediate_signal.clone()); let dimensions = create_blocking_resource( move || {}, @@ -290,14 +355,19 @@ pub fn Dimensions() -> impl IntoView { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - {move || { + { + move || { let value = dimensions.get(); let total_items = match value { Some(v) => v.len().to_string(), _ => "0".to_string(), }; view! { - <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> + <Stat + heading="Dimensions" + icon="ri-ruler-2-fill" + number={total_items} + /> } }} <ModalComponent @@ -310,14 +380,17 @@ pub fn Dimensions() -> impl IntoView { <div class="card-body"> <div class="flex justify-between mb-2"> <h2 class="card-title">Dimensions</h2> - <Button - text="Create Dimension".to_string() - on_click=|_| modal_action("my_modal_5", "open") - /> + <Button text="Create Dimension".to_string() on_click={ + let edit_clone = edit_signal.to_owned(); + move |_| { + edit_clone.set(false); + modal_action("my_modal_5","open") + }}/> </div> <div> - {move || { + { + move || { let value = dimensions.get(); let settings = TableSettings { redirect_prefix: None, @@ -326,7 +399,14 @@ pub fn Dimensions() -> impl IntoView { Some(v) => { let data = v .iter() - .map(|ele| { json!(ele).as_object().unwrap().clone() }) + .map(|ele| { + let mut ele_map = json!(ele).as_object().unwrap().clone(); + ele_map.insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()) + ); + ele_map + }) .collect::<Vec<Map<String, Value>>>() .to_owned(); view! { @@ -349,4 +429,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file From ba2d33b59f00f776266dec2777c7c29c734024e1 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Mon, 18 Dec 2023 21:13:31 +0530 Subject: [PATCH 217/352] fix: context parsing --- Cargo.lock | 1 + crates/frontend/Cargo.toml | 1 + .../src/components/side_nav/side_nav.rs | 35 ++++++++++-- crates/frontend/src/pages/Experiment/mod.rs | 54 +++++++++---------- crates/frontend/src/pages/Home/Home.rs | 8 ++- 5 files changed, 64 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56fb7a1ef..0acacea64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1435,6 +1435,7 @@ dependencies = [ "chrono", "console_error_panic_hook", "derive_more", + "dotenv", "futures", "http", "js-sys", diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 57dae92f5..ad0ef4063 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] +dotenv = { workspace = true } actix-files = { version = "0.6", optional = true } actix-web = { version = "4", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index be5d95845..28f4dbbd2 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -1,8 +1,22 @@ +use std::{collections::HashSet, str::FromStr, env::VarError}; + use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; use leptos::{logging::log, *}; use leptos_router::{use_location, use_navigate, A}; +fn get_from_env_unsafe<F>(name: &str) -> Result<F, VarError> +where + F: FromStr, + <F as FromStr>::Err: std::fmt::Debug, +{ + std::env::var(name) + .map(|val| val.parse().unwrap()) + .map_err(|e| { + return e; + }) +} + #[component] pub fn SideNav() -> impl IntoView { let location = use_location(); @@ -10,6 +24,21 @@ pub fn SideNav() -> impl IntoView { let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); let (app_routes, set_app_routes) = create_signal(create_routes(&tenant_rs.get())); + let tenants: HashSet<String> = get_from_env_unsafe::<String>("TENANTS") + .unwrap_or("m,s".into()) + .split(",") + .map(|tenant| tenant.to_string()) + .collect::<HashSet<String>>(); + + let mut view_vector = vec![]; + for tenant in tenants.into_iter() { + if tenant == tenant_rs.get() { + view_vector.push(view! { <option selected={true}>{tenant}</option> }.into_view()); + } else { + view_vector.push(view! { <option>{tenant}</option> }.into_view()); + } + } + create_effect(move |_| { let current_path = location.pathname.get(); @@ -58,13 +87,11 @@ pub fn SideNav() -> impl IntoView { Default::default(), ); } - class="select w-full max-w-xs shadow-md" > - <option selected>mjos</option> - <option>sdk_config</option> + { view_vector } </select> - <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> + // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> <ul class="menu"> <For diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 6068d9dbb..b48165dbb 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -14,7 +14,7 @@ use crate::{ table::Table, types::{Column, TableSettings}, }, - }, + }, pages::Home::Home::extract_and_format, }; #[derive( @@ -159,10 +159,7 @@ fn experiment_detail_view( initial_data: Experiment, exp_resource: Resource<(String, String), Result<Experiment, String>>, ) -> impl IntoView { - let contexts = initial_data.context["and"] - .as_array() - .unwrap_or(&Vec::new()) - .to_owned(); + let contexts = extract_and_format(&initial_data.context); let (experiment, _) = create_signal(initial_data); let (ctxs, _) = create_signal(contexts); @@ -291,34 +288,31 @@ fn experiment_detail_view( <h2 class="card-title">Context</h2> <div class="flex flex-row flex-wrap gap-2"> {move || { - let contexts = move || ctxs.get().into_iter(); - let mut view: Vec<HtmlElement<html::Div>> = Vec::new(); - for item in contexts() { - for (_, value) in item.as_object().unwrap().into_iter() { - let rule_vector = value.as_array().unwrap(); - let mut rule_iter = rule_vector.to_owned().into_iter(); - let (var, value) = ( - rule_iter.next().unwrap(), - rule_iter.next().unwrap(), - ); - let dimension = var.as_object().unwrap().get("var").unwrap(); - view.push( - view! { - <div class="stat w-3/12"> - <div class="stat-title"> - {format!("{}", dimension.as_str().unwrap())} - </div> - <div class="stat-value text-base"> - {format!("{}", value.as_str().unwrap())} - </div> + let context = ctxs.get(); + let mut view = Vec::new(); + let tokens = context.split("&&"); + for token in tokens.into_iter() { + let mut t = token.trim().split(" "); + let (dimension, _, value) = ( + t.next(), + t.next(), + t.next() + ); + view.push( + view! { + <div class="stat w-3/12"> + <div class="stat-title"> + {format!("{}", dimension.unwrap())} </div> - }, - ) - } + <div class="stat-value text-base"> + {format!("{}", &value.unwrap()[1..value.unwrap().chars().count()])} + </div> + </div> + }, + ); } view - }} - + }} </div> </div> </div> <div class="card bg-base-100 max-w-screen shadow m-5"> diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index c85c3d48b..0dfa5c497 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -410,7 +410,13 @@ pub fn home() -> impl IntoView { view! { <tr> <td>{config}</td> - <td>{value.as_str().unwrap().to_string()}</td> + <td>{match value { + Value::String(s) => s, + Value::Number(num) => num.to_string(), + Value::Bool(b) => b.to_string(), + _ => "".into() + }}</td> + </tr> } } From 41905a6c882894452701ec60330802605f9791fa Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Sat, 16 Dec 2023 00:15:57 +0530 Subject: [PATCH 218/352] fix: fixed experiment list page feedback --- .../experimentation-platform/src/db/schema.rs | 2 +- crates/frontend/src/api.rs | 10 +- .../condition_pills/condition_pills.rs | 30 +++ .../src/components/condition_pills/mod.rs | 2 + .../src/components/condition_pills/utils.rs | 113 +++++++++ .../components/context_form/context_form.rs | 49 ++-- .../src/components/context_form/mod.rs | 2 +- .../src/components/context_form/utils.rs | 8 +- .../experiment_form/experiment_form.rs | 232 +++++++++++------- .../src/components/experiment_form/mod.rs | 2 +- .../src/components/experiment_form/types.rs | 6 +- .../src/components/experiment_form/utils.rs | 8 +- crates/frontend/src/components/mod.rs | 3 +- .../src/components/nav_item/nav_item.rs | 2 +- .../components/override_form/override_form.rs | 34 +-- .../src/components/side_nav/side_nav.rs | 5 +- crates/frontend/src/components/stat/mod.rs | 2 +- crates/frontend/src/components/stat/stat.rs | 8 +- crates/frontend/src/components/table/table.rs | 32 +-- .../pages/ContextOverride/ContextOverride.rs | 49 ++-- .../src/pages/DefaultConfig/DefaultConfig.rs | 14 +- .../src/pages/Dimensions/Dimensions.rs | 7 +- crates/frontend/src/pages/Experiment/mod.rs | 16 +- .../pages/ExperimentList/ExperimentList.rs | 102 +++----- .../src/pages/ExperimentList/types.rs | 2 +- .../src/pages/ExperimentList/utils.rs | 121 +++++++++ crates/frontend/src/pages/Home/Home.rs | 16 +- 27 files changed, 546 insertions(+), 331 deletions(-) create mode 100644 crates/frontend/src/components/condition_pills/condition_pills.rs create mode 100644 crates/frontend/src/components/condition_pills/mod.rs create mode 100644 crates/frontend/src/components/condition_pills/utils.rs diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index c10c6d338..796f436a7 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -246,4 +246,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2024m06, event_log_y2024m07, experiments, -); \ No newline at end of file +); diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 1b6ba6e3a..a2847ac0c 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,6 +1,6 @@ use leptos::*; -use crate::pages::ExperimentList::types::{Dimension, DefaultConfig}; +use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; // #[derive(Debug, Serialize, Deserialize, Clone)] // pub struct Dimension { @@ -46,7 +46,9 @@ pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, } } -pub fn dimension_resource(tenant: ReadSignal<String>) -> Resource<String, Vec<Dimension>> { +pub fn dimension_resource( + tenant: ReadSignal<String>, +) -> Resource<String, Vec<Dimension>> { create_blocking_resource( move || tenant.get(), |tenant| async { @@ -58,7 +60,9 @@ pub fn dimension_resource(tenant: ReadSignal<String>) -> Resource<String, Vec<Di ) } -pub fn default_config_resource(tenant: ReadSignal<String>) -> Resource<String, Vec<DefaultConfig>> { +pub fn default_config_resource( + tenant: ReadSignal<String>, +) -> Resource<String, Vec<DefaultConfig>> { create_blocking_resource( move || tenant.get(), |tenant| async { diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs new file mode 100644 index 000000000..fd99ad75f --- /dev/null +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -0,0 +1,30 @@ +use super::utils::{extract_and_format, parse_conditions}; +use leptos::*; +use serde_json::Value; + +#[component] +pub fn ConditionPills(context: Value) -> impl IntoView { + let condition = extract_and_format(&context); + let ctx_values = parse_conditions(condition.clone()); + + view! { + {ctx_values + .into_iter() + .map(|(dim, op, val)| { + view! { + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> + <span class="font-mono font-medium context_condition text-gray-500"> + {dim} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {op} + </span> + <span class="font-mono font-semibold context_condition"> + {val} + </span> + </span> + } + }) + .collect::<Vec<_>>()} + } +} diff --git a/crates/frontend/src/components/condition_pills/mod.rs b/crates/frontend/src/components/condition_pills/mod.rs new file mode 100644 index 000000000..bad1e7b1e --- /dev/null +++ b/crates/frontend/src/components/condition_pills/mod.rs @@ -0,0 +1,2 @@ +pub mod condition_pills; +pub mod utils; diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs new file mode 100644 index 000000000..c7a61bbb6 --- /dev/null +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -0,0 +1,113 @@ +use serde_json::Value; + +pub fn extract_and_format(condition: &Value) -> String { + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + let mut formatted_conditions = Vec::new(); + for cond in conditions_json { + formatted_conditions.push(format_condition(cond)); + } + + formatted_conditions.join(" and ") + } else { + // Handling single conditions + format_condition(condition) + } +} + +fn format_condition(condition: &Value) -> String { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return format!("{} {} {}", left_str, operator, var_str); + } + } + + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + if let Some(value) = operands.get(1) { + if value.is_string() { + return format!( + "{} {} \"{}\"", + key, + operator, + value.as_str().unwrap() + ); + } else { + return format!("{} {} {}", key, operator, value); + } + } + } + } + } + + "Invalid Condition".to_string() +} + +pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { + let mut conditions = Vec::new(); + let operators = vec!["==", "in"]; + + // Split the string by "and" and iterate over each condition + for condition in input.split("and") { + let mut parts = Vec::new(); + let mut operator_found = ""; + + // Check for each operator + for operator in &operators { + if condition.contains(operator) { + operator_found = operator; + parts = condition.split(operator).map(|s| s.trim()).collect(); + + // TODO: add this when context update is enabled + if parts.len() == 2 && operator == &"in" { + parts.swap(0, 1); + } + + break; + } + } + + if parts.len() == 2 { + let mut key = parts[0].to_string(); + let mut op = operator_found.to_string(); + let mut val = parts[1].to_string(); + // Add a space after key + key.push(' '); + if op == "==".to_string() { + val = val.trim_matches('"').to_string(); + op = "is".to_string(); + } else { + val = val.trim_matches('"').to_string(); + op = "has".to_string(); + } + op.push(' '); + + conditions.push((key, op, val)); + } + } + + conditions +} diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index ad193f375..c8dd5e1bd 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -2,9 +2,7 @@ use crate::pages::ExperimentList::types::Dimension; use leptos::*; use std::cmp; use std::collections::HashSet; -use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, HtmlSelectElement, SubmitEvent, MouseEvent}; -use serde_json::json; +use web_sys::MouseEvent; #[component] pub fn ContextForm<NF>( @@ -14,26 +12,12 @@ pub fn ContextForm<NF>( context: Vec<(String, String, String)>, ) -> impl IntoView where - NF: Fn(Vec<(String, String, String)>) + 'static + NF: Fn(Vec<(String, String, String)>) + 'static, { let has_dimensions = dimensions.len() > 0; - let (default_condition, default_used_dimension) = if context.len() == 0 && has_dimensions { - let dimension = dimensions[0].dimension.to_string(); - ( - vec![(dimension.to_string(), "".to_string(), "".to_string())], - vec![dimension.to_string()] - ) - } else { - ( - context.clone(), - context.into_iter().map(|item| item.0.to_string()).collect::<Vec<String>>() - ) - }; - - let (context, set_context) = create_signal(default_condition); - let (used_dimensions, set_used_dimensions) = create_signal(HashSet::from_iter(default_used_dimension)); - let total_dimensions = dimensions.len(); + let (context, set_context) = create_signal(context.clone()); + let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); let last_idx = create_memo(move |_| { let len = context.get().len(); @@ -62,7 +46,7 @@ where <div class="dropdown dropdown-left"> <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> <i class="ri-add-line"></i> - Add Dimension + Add Context </label> <ul tabindex="0" @@ -111,6 +95,16 @@ where </div> </div> </div> + <Show when=move || context.get().len() == 0> + <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> + <div> + <i class="ri-add-circle-line text-xl"></i> + </div> + <div> + <span class="text-semibold text-sm">Add Context</span> + </div> + </div> + </Show> <div class="p-4"> <For each=move || { @@ -147,17 +141,14 @@ where <option disabled selected> Pick one </option> - <option value="==">==</option> - <option value="!=">!=</option> - <option value="IN">IN</option> + <option value="==">"IS"</option> + <option value="IN">"HAS"</option> </select> </div> <div class="form-control"> - <label class="label capitalize font-mono text-sm"> - <span class="label-text" name="context-dimension-name"> - {dimension_label} - </span> + <label class="label font-mono text-sm"> + <span class="label-text" name="context-dimension-name">{dimension_label}</span> </label> <div class="flex gap-x-6 items-center"> <input @@ -222,4 +213,4 @@ where </Show> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/context_form/mod.rs b/crates/frontend/src/components/context_form/mod.rs index d7910011f..6f9238576 100644 --- a/crates/frontend/src/components/context_form/mod.rs +++ b/crates/frontend/src/components/context_form/mod.rs @@ -1,2 +1,2 @@ pub mod context_form; -pub mod utils; \ No newline at end of file +pub mod utils; diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 4228c54e4..18021d731 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,9 +1,7 @@ -use serde_json::{json, Value, Map}; use reqwest::{Error, StatusCode}; +use serde_json::{json, Map, Value}; -pub fn construct_context( - conditions: Vec<(String, String, String)>, -) -> Value { +pub fn construct_context(conditions: Vec<(String, String, String)>) -> Value { let context = if conditions.len() == 1 { // Single condition let (variable, operator, value) = &conditions[0]; @@ -74,4 +72,4 @@ pub async fn create_context( StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), _ => Err("Internal Server Error".to_string()), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 78e120c62..8698736b7 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,4 +1,5 @@ use super::utils::create_experiment; +use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::Button::Button::Button; use crate::components::{ context_form::context_form::ContextForm, override_form::override_form::OverrideForm, @@ -6,28 +7,71 @@ use crate::components::{ use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; +use chrono::offset::Local; +use futures::join; use leptos::*; +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::sync::RwLock; -use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, MouseEvent, SubmitEvent}; +use web_sys::MouseEvent; + +fn default_variants_for_form() -> Vec<(String, Variant)> { + vec![ + ( + "control-variant".to_string(), + Variant { + id: "control".to_string(), + variant_type: VariantType::CONTROL, + context_id: None, + override_id: None, + overrides: Map::new(), + }, + ), + ( + "experimental-variant".to_string(), + Variant { + id: "experimental".to_string(), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + }, + ), + ] +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, +} #[component] pub fn ExperimentForm<NF>( name: String, context: Vec<(String, String, String)>, variants: Vec<Variant>, - dimensions: Vec<Dimension>, - default_config: Vec<DefaultConfig>, handle_submit: NF, + default_config: Vec<DefaultConfig>, + dimensions: Vec<Dimension>, + edit_mode: bool, ) -> impl IntoView where NF: Fn() + 'static + Clone, { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let default_variants = if variants.len() == 0 { + default_variants_for_form() + } else { + variants + .into_iter() + .map(|variant| (variant.id.to_string(), variant)) + .collect::<Vec<(String, Variant)>>() + }; + let (experiment_name, set_experiment_name) = create_signal(name); let (f_context, set_context) = create_signal(context.clone()); - let (f_variants, set_variants) = create_signal(variants); + let (f_variants, set_variants) = create_signal(default_variants); let handle_context_form_change = move |updated_ctx: Vec<(String, String, String)>| { set_context.set(updated_ctx); @@ -36,7 +80,7 @@ where let handle_override_form_change = move |variant_idx: usize| { let handle_change = move |updated_overrides: Map<String, Value>| { set_variants.update(|curr_variants| { - curr_variants[variant_idx].overrides = updated_overrides.clone(); + curr_variants[variant_idx].1.overrides = updated_overrides.clone(); }); }; handle_change @@ -45,15 +89,19 @@ where let on_submit = move |event: MouseEvent| { event.prevent_default(); logging::log!("Submitting experiment form"); + logging::log!("{:?}", f_variants.get()); let f_experiment_name = experiment_name.get(); let f_context = f_context.get(); - let f_variants = f_variants.get(); + let f_variants = f_variants + .get() + .into_iter() + .map(|(_, variant)| variant) + .collect::<Vec<Variant>>(); let tenant = tenant_rs.get(); let handle_submit_clone = handle_submit.clone(); logging::log!("{:?}", f_experiment_name); - logging::log!("{:?}", f_variants); logging::log!("{:?}", f_context); spawn_local({ @@ -63,7 +111,7 @@ where .await; match result { - Ok(value) => { + Ok(_) => { handle_submit_clone(); } Err(_) => { @@ -107,7 +155,6 @@ where <span class="label-text font-semibold text-base">Variants</span> </label> - // make some more random default id for this can lead to undefined behaviour if we use index based ids <button class="btn btn-outline btn-sm text-xs m-1" on:click:undelegated=move |_| { @@ -115,97 +162,108 @@ where set_variants .update(|curr_variants| { let total_variants = curr_variants.len(); + let key = Local::now().timestamp().to_string(); curr_variants - .push(Variant { - id: format!("variant-{}", total_variants), - variant_type: VariantType::EXPERIMENTAL, - context_id: None, - override_id: None, - overrides: Map::new(), - }) + .push(( + key, + Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + } + )) }); } > - <i class="ri-add-line"></i> Add Variant </button> </div> - <For - each=move || { - f_variants.get().into_iter().enumerate().collect::<Vec<(usize, Variant)>>() - } + <For + each=move || { + f_variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, Variant))>>() + } + key=|(_, (key, _))| key.to_string() + children=move |(idx, (_, variant))| { + let variant_clone = variant.clone(); + let default_config = default_config.clone(); + let handle_change = handle_override_form_change(idx); - key=|(idx, variant)| format!("{}-{}", variant.id.to_string(), idx) - children=move |(idx, variant)| { - let config = default_config.clone(); - let variant_clone = variant.clone(); - let handle_change = handle_override_form_change(idx); - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=move || variant.variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants - .update(|value| { - value[idx] = new_variant; + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants.update(|value| { + value[idx].1 = new_variant; }) - } + } - class="select select-bordered" - > - <option disabled selected> - Pick one - </option> - <option value=VariantType::CONTROL - .to_string()>{VariantType::CONTROL.to_string()}</option> - <option value=VariantType::EXPERIMENTAL - .to_string()> - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> + class="select select-bordered" + > + <option disabled> + Pick one + </option> + <option + value=VariantType::CONTROL.to_string() + selected={VariantType::CONTROL == variant.variant_type} + > + {VariantType::CONTROL.to_string()} + </option> + <option + value=VariantType::EXPERIMENTAL.to_string() + selected={VariantType::EXPERIMENTAL == variant.variant_type} + > + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> + </div> + </div> + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=default_config + handle_change=handle_change + is_standalone=false + /> </div> </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=config - handle_change=handle_change - is_standalone=false - /> - </div> - </div> + } } - } - /> - + /> </div> - <div class="flex justify-end"> - <Button text="Submit".to_string() on_click=on_submit/> + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click = on_submit /> </div> </div> } diff --git a/crates/frontend/src/components/experiment_form/mod.rs b/crates/frontend/src/components/experiment_form/mod.rs index 39a23b7d7..ccf974665 100644 --- a/crates/frontend/src/components/experiment_form/mod.rs +++ b/crates/frontend/src/components/experiment_form/mod.rs @@ -1,3 +1,3 @@ pub mod experiment_form; pub mod types; -pub mod utils; \ No newline at end of file +pub mod utils; diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs index 06433fdac..0ae50e734 100644 --- a/crates/frontend/src/components/experiment_form/types.rs +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -1,8 +1,8 @@ -use serde_json::Value; -use serde::Serialize; use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; +use serde::Serialize; +use serde_json::Value; #[derive(Serialize)] pub struct ExperimentCreateRequest { @@ -10,4 +10,4 @@ pub struct ExperimentCreateRequest { pub context: Value, pub variants: Vec<Variant>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index 9065354aa..8e2406e69 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -1,14 +1,14 @@ -use serde_json::{json, Value}; -use crate::pages::ExperimentList::types::Variant; use super::types::ExperimentCreateRequest; use crate::components::context_form::utils::construct_context; +use crate::pages::ExperimentList::types::Variant; use reqwest::StatusCode; +use serde_json::{json, Value}; pub async fn create_experiment( conditions: Vec<(String, String, String)>, variants: Vec<Variant>, name: String, - tenant: String + tenant: String, ) -> Result<String, String> { let payload = ExperimentCreateRequest { name: name, @@ -33,4 +33,4 @@ pub async fn create_experiment( StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), _ => Err("Internal Server Error".to_string()), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 0b5597343..d0d257a37 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,9 +1,10 @@ pub mod Button; +pub mod condition_pills; pub mod context_form; pub mod experiment_form; pub mod nav_item; pub mod override_form; pub mod pagination; pub mod side_nav; +pub mod stat; pub mod table; -pub mod stat; \ No newline at end of file diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 0aaa4b1d6..5efd90a60 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -31,4 +31,4 @@ pub fn NavItem( <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> </A> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 88ec35435..92b81b82e 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,34 +1,22 @@ use crate::pages::ExperimentList::types::DefaultConfig; use leptos::*; -use serde_json::{Map, Value, json}; +use serde_json::{json, Map, Value}; use std::collections::HashSet; -use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, HtmlSelectElement, SubmitEvent, MouseEvent}; +use web_sys::MouseEvent; #[component] pub fn OverrideForm<NF>( overrides: Map<String, Value>, default_config: Vec<DefaultConfig>, handle_change: NF, - is_standalone: bool + is_standalone: bool, ) -> impl IntoView where - NF: Fn(Map<String, Value>) + 'static + NF: Fn(Map<String, Value>) + 'static, { let has_default_config = default_config.len() != 0; - let (default_overrides, default_used_config_keys): (Map<String, Value>, Vec<String>) = if overrides.len() == 0 && has_default_config { - ( - Map::from_iter([(default_config[0].key.to_string(), json!(""))]), - vec![default_config[0].key.to_string()] - ) - } else { - ( - overrides.clone(), - overrides.keys().map(String::from).collect::<Vec<String>>() - ) - }; - let (overrides, set_overrides) = create_signal(default_overrides); - let (used_config_keys, set_used_config_keys) = create_signal(HashSet::from_iter(default_used_config_keys)); + let (overrides, set_overrides) = create_signal(overrides.clone()); + let (used_config_keys, set_used_config_keys) = create_signal(HashSet::new()); let on_submit = move |event: MouseEvent| { event.prevent_default(); @@ -45,13 +33,13 @@ where <div class="space-y-4"> <div class="flex items-center justify-between gap-4"> <label class="label"> - <span class="label-text font-semibold text-base">Override</span> + <span class="label-text font-semibold text-base">Overrides</span> </label> <div> <div class="dropdown dropdown-left"> <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> <i class="ri-add-line"></i> - Add Config Key + Add Override </label> <ul tabindex="0" @@ -98,10 +86,10 @@ where <Show when=move || overrides.get().len() == 0> <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> <div> - <i class="ri-windy-line text-3xl"></i> + <i class="ri-add-circle-line text-xl"></i> </div> <div> - <span class="text-semibold">Add config keys</span> + <span class="text-semibold text-sm">Add Override</span> </div> </div> </Show> @@ -172,4 +160,4 @@ where </Show> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 28f4dbbd2..fa912e3d8 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, str::FromStr, env::VarError}; +use std::{collections::HashSet, env::VarError, str::FromStr}; use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; @@ -33,7 +33,8 @@ pub fn SideNav() -> impl IntoView { let mut view_vector = vec![]; for tenant in tenants.into_iter() { if tenant == tenant_rs.get() { - view_vector.push(view! { <option selected={true}>{tenant}</option> }.into_view()); + view_vector + .push(view! { <option selected={true}>{tenant}</option> }.into_view()); } else { view_vector.push(view! { <option>{tenant}</option> }.into_view()); } diff --git a/crates/frontend/src/components/stat/mod.rs b/crates/frontend/src/components/stat/mod.rs index 2b9cad20b..4f22af63a 100644 --- a/crates/frontend/src/components/stat/mod.rs +++ b/crates/frontend/src/components/stat/mod.rs @@ -1 +1 @@ -pub mod stat; \ No newline at end of file +pub mod stat; diff --git a/crates/frontend/src/components/stat/stat.rs b/crates/frontend/src/components/stat/stat.rs index 484063195..d433497af 100644 --- a/crates/frontend/src/components/stat/stat.rs +++ b/crates/frontend/src/components/stat/stat.rs @@ -1,11 +1,7 @@ use leptos::*; #[component] -pub fn Stat( - heading: &'static str, - icon: &'static str, - number: String, -) -> impl IntoView { +pub fn Stat(heading: &'static str, icon: &'static str, number: String) -> impl IntoView { let icon_class = format!("{} text-5xl", icon); view! { <div class="stats shadow"> @@ -18,4 +14,4 @@ pub fn Stat( </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index bf97f68da..373adecfa 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -1,8 +1,5 @@ -use crate::components::table::types::TableSettings; - use super::types::Column; use leptos::*; -use leptos_router::use_navigate; use serde_json::{json, Map, Value}; fn generate_table_row_str(row: &Value) -> String { @@ -26,9 +23,7 @@ pub fn Table( table_style: String, columns: Vec<Column>, rows: Vec<Map<String, Value>>, - settings: TableSettings, ) -> impl IntoView { - let (redirect_info, _) = create_signal(settings.redirect_prefix); view! { <div class="overflow-x-auto"> <table class="table table-zebra"> @@ -39,7 +34,7 @@ pub fn Table( {columns .iter() .filter(|column| !column.hidden) - .map(|column| view! { <th class="uppercase">{&column.name}</th> }) + .map(|column| view! { <th class="uppercase">{&column.name.replace("_", " ")}</th> }) .collect_view()} </tr> @@ -51,34 +46,13 @@ pub fn Table( .enumerate() .map(|(index, row)| { let row_id = row - .get("id") + .get(&key_column) .unwrap_or(&json!("")) .as_str() .unwrap() .to_string(); view! { - <tr - on:click=move |_| { - let redirect_info = redirect_info.get(); - if redirect_info.is_some() { - let prefix = redirect_info.unwrap(); - let nav = use_navigate(); - nav( - format!("{prefix}/{row_id}").as_str(), - Default::default(), - ); - } - } - - style:cursor=move || { - if redirect_info.get().is_some() { - "pointer" - } else { - "default" - } - } - > - + <tr id={row_id}> <th>{index + 1}</th> {columns diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 12b9294f4..a41e6880d 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,21 +1,19 @@ -use std::collections::HashMap; use std::rc::Rc; -use crate::api::{fetch_dimensions, fetch_default_config}; +use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; -use crate::components::table::types::TableSettings; use crate::components::table::{table::Table, types::Column}; use crate::components::Button::Button::Button; use crate::pages::DefaultConfig::types::Config; // use leptos::spawn_local; +use crate::components::condition_pills::condition_pills::ConditionPills; use crate::utils::modal_action; use leptos::*; use leptos_router::use_query_map; -use reqwest::{Error, StatusCode}; +use reqwest::StatusCode; use serde_json::{json, Map, Value}; -use wasm_bindgen::JsCast; -use web_sys::{HtmlDialogElement, MouseEvent}; +use web_sys::MouseEvent; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); @@ -469,14 +467,9 @@ pub fn ContextOverride() -> impl IntoView { match result { Some(Ok(config)) => { let mut contexts: Vec<Map<String, Value>> = Vec::new(); - let settings = TableSettings { - redirect_prefix: None, - }; let mut context_views = Vec::new(); let mut override_signal = Map::new(); for context in config.contexts.iter() { - let condition = extract_and_format(&context.condition); - let ctx_values = parse_conditions(condition.clone()); for key in context.override_with_keys.iter() { let mut map = Map::new(); let ovr = config.overrides.get(key).unwrap(); @@ -499,33 +492,14 @@ pub fn ContextOverride() -> impl IntoView { .push( view! { <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6 shadow-md"> - <div class="flex justify-between"> <div class="flex items-center space-x-4"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white shadow-md font-mono"> + <h3 class="card-title text-base timeline-box text-gray-800 dark:text-white bg-white shadow-md font-mono"> "Condition" - </h2> + </h3> <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> - {ctx_values - .into_iter() - .map(|(dim, op, val)| { - view! { - <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> - <span class="font-mono font-medium context_condition text-gray-500"> - {dim} - </span> - <span class="font-mono font-medium text-gray-650 context_condition "> - {op} - </span> - <span class="font-mono font-semibold context_condition"> - {val} - </span> - </span> - } - }) - .collect_view()} - + <ConditionPills context={context.condition.clone()} /> </div> <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> <i class="ri-edit-line text-blue-500"></i> @@ -537,7 +511,6 @@ pub fn ContextOverride() -> impl IntoView { rows=contexts.clone() key_column="id".to_string() columns=table_columns.get() - settings=settings.clone() /> </div> @@ -558,7 +531,13 @@ pub fn ContextOverride() -> impl IntoView { }, ] } - None => vec![view! { <div>Loading....</div> }], + None => { + vec![ + view! { + <div>Loading....</div> + }, + ] + } } }) }} diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 28b2f8890..3db5df04e 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -1,10 +1,7 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::components::table::{ - table::Table, - types::{Column, TableSettings}, -}; +use crate::components::table::{table::Table, types::Column}; use crate::components::stat::stat::Stat; use crate::components::Button::Button::Button; @@ -191,7 +188,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { <form - class="form-control w-full overflow-auto space-y-4 bg-white text-gray-700 font-mono" + class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" on:submit=on_submit > <div class="form-control"> @@ -317,7 +314,8 @@ fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); }; - let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; + let edit_icon: HtmlElement<html::I> = + view! { <i class="ri-pencil-line ri-xl text-blue-500 cursor-pointer"></i> }; view! { <span on:click = edit_click_handler>{edit_icon}</span> }.into_view() } @@ -375,9 +373,6 @@ pub fn DefaultConfig() -> impl IntoView { move || { let default_config = default_config_resource.get().unwrap_or(vec![]); let total_default_config_keys = default_config.len().to_string(); - let table_settings = TableSettings { - redirect_prefix: None - }; let edit_signal = edit_signal.clone(); @@ -416,7 +411,6 @@ pub fn DefaultConfig() -> impl IntoView { rows=table_rows key_column="id".to_string() columns=table_columns.get() - settings=table_settings /> </div> </div> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 474ba4787..3aa30fcb0 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -329,7 +329,6 @@ pub fn Dimensions() -> impl IntoView { let edit_signal = create_rw_signal(true); provide_context(edit_signal); - let dimensions = create_blocking_resource( move || {}, |_value| async move { @@ -392,9 +391,6 @@ pub fn Dimensions() -> impl IntoView { { move || { let value = dimensions.get(); - let settings = TableSettings { - redirect_prefix: None, - }; match value { Some(v) => { let data = v @@ -415,7 +411,6 @@ pub fn Dimensions() -> impl IntoView { rows=data key_column="id".to_string() columns=table_columns.get() - settings=settings /> } } @@ -429,4 +424,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index b48165dbb..0a562ba4b 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -10,11 +10,9 @@ use crate::{ api::{fetch_default_config, fetch_dimensions}, components::{ experiment_form::experiment_form::ExperimentForm, - table::{ - table::Table, - types::{Column, TableSettings}, - }, - }, pages::Home::Home::extract_and_format, + table::{table::Table, types::Column}, + }, + pages::Home::Home::extract_and_format, }; #[derive( @@ -305,7 +303,7 @@ fn experiment_detail_view( {format!("{}", dimension.unwrap())} </div> <div class="stat-value text-base"> - {format!("{}", &value.unwrap()[1..value.unwrap().chars().count()])} + {format!("{}", &value.unwrap()[1..value.unwrap().chars().count() - 1])} </div> </div> }, @@ -323,9 +321,6 @@ fn experiment_detail_view( let exp = move || experiment.get(); let rows = gen_variant_rows(&exp().variants).unwrap(); let mut columns: Vec<Column> = Vec::new(); - let settings = TableSettings { - redirect_prefix: None, - }; columns.push(Column::default("Variant".into())); for okey in exp().override_keys.as_array().unwrap().into_iter() { columns.push(Column::default(okey.as_str().unwrap().into())); @@ -336,7 +331,6 @@ fn experiment_detail_view( rows=rows key_column="overrides".to_string() columns=columns - settings=settings /> } }} @@ -521,4 +515,4 @@ fn add_dialogs( }.into_view(), ExperimentStatusType::CONCLUDED => view! {}.into_view(), } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 9d5e7dcd8..2adfadf59 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -6,31 +6,29 @@ use chrono::{prelude::Utc, TimeZone}; use serde::{Deserialize, Serialize}; use crate::components::{ - experiment_form::experiment_form::ExperimentForm, - pagination::pagination::Pagination, - stat::stat::Stat, - table::{ - table::Table, - types::{Column, TableSettings}, - }, + experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, + stat::stat::Stat, table::table::Table, Button::Button::Button, }; -#[derive(Serialize, Deserialize, Clone, Debug)] -struct CombinedResource { - experiments: ExperimentsResponse, - dimensions: Vec<Dimension>, - default_config: Vec<DefaultConfig>, -} - use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; use super::{ types::{DefaultConfig, Dimension}, - utils::{fetch_default_config, fetch_dimensions, fetch_experiments}, + utils::{ + experiment_table_columns, fetch_default_config, fetch_dimensions, + fetch_experiments, + }, }; use serde_json::{json, Map, Value}; use wasm_bindgen::JsCast; +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + experiments: ExperimentsResponse, + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, +} + #[component] pub fn ExperimentList() -> impl IntoView { // acquire tenant @@ -44,34 +42,7 @@ pub fn ExperimentList() -> impl IntoView { }); let (open_form_modal, set_open_form_modal) = create_signal(false); - let table_columns = create_memo(move |_| { - vec![ - Column::default("id".to_string()), - Column::default("name".to_string()), - Column::new( - "status".to_string(), - None, - Some(|value: &str, _| { - let badge_color = match value { - "CREATED" => "badge-info", - "INPROGRESS" => "badge-warning", - "CONCLUDED" => "badge-success", - &_ => "info", - }; - let class = format!("badge {}", badge_color); - view! { - <div class={class}> - <span class="text-white font-semibold text-xs"> - {value.to_string()} - </span> - </div> - } - .into_view() - }), - ), - Column::default("context".to_string()), - ] - }); + let table_columns = create_memo(move |_| experiment_table_columns()); let combined_resource: Resource<(String, ListFilters), CombinedResource> = create_blocking_resource( @@ -143,9 +114,8 @@ pub fn ExperimentList() -> impl IntoView { <div class="flex justify-between"> <h2 class="card-title">Experiments</h2> <div> - <button - class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" - on:click=move |event: web_sys::MouseEvent| { + <Button + on_click=move |event: web_sys::MouseEvent| { event.prevent_default(); set_open_form_modal.set(true); if let Some(element) = document() @@ -155,31 +125,18 @@ pub fn ExperimentList() -> impl IntoView { let dialog_ele = element .dyn_ref::<web_sys::HtmlDialogElement>(); match dialog_ele { - Some(ele) => { - ele.show_modal(); - } - None => { - log!("no modal element"); - } + Some(ele) => { let _ = ele.show_modal(); }, + None => { log!("no modal element"); } } } } - > - - Create Experiment - <i class="ri-edit-2-line ml-2"></i> - </button> + text="Create Experiment".to_string() + /> </div> </div> <div> {move || { - let current_tenant = tenant_rs.get(); - let settings = TableSettings { - redirect_prefix: Some( - format!("admin/{current_tenant}/experiments"), - ), - }; let value = combined_resource.get(); match value { Some(v) => { @@ -187,7 +144,20 @@ pub fn ExperimentList() -> impl IntoView { .experiments .data .iter() - .map(|ele| { json!(ele).as_object().unwrap().clone() }) + .map(|ele| { + let mut ele_map = json!(ele).as_object().unwrap().to_owned(); + ele_map + .insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()) + ); + ele_map + .insert( + "last_modified".to_string(), + json!(ele.last_modified.format("%v").to_string()) + ); + ele_map + }) .collect::<Vec<Map<String, Value>>>() .to_owned(); view! { @@ -196,7 +166,6 @@ pub fn ExperimentList() -> impl IntoView { rows=data key_column="id".to_string() columns=table_columns.get() - settings=settings /> } } @@ -313,6 +282,7 @@ pub fn ExperimentList() -> impl IntoView { dimensions=dim.clone() default_config=def_conf.clone() handle_submit=handle_submit_experiment_form + edit_mode=false /> </div> </div> @@ -324,4 +294,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index 34b558f18..cb6d029ad 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -81,4 +81,4 @@ pub struct Variant { pub context_id: Option<String>, pub override_id: Option<String>, pub overrides: Map<String, Value>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 38e19fbc4..110535942 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,5 +1,13 @@ use super::types::{DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; +use crate::components::{ + condition_pills::condition_pills::ConditionPills, table::types::Column, +}; +use core::time::Duration; +use leptos::*; +use leptos_router::A; +use serde_json::{json, Map, Value}; use std::vec::Vec; +use web_sys::MouseEvent; pub async fn fetch_experiments( filters: ListFilters, @@ -93,3 +101,116 @@ pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { Ok(response) } + +pub fn experiment_table_columns() -> Vec<Column> { + vec![ + Column::new( + "name".to_string(), + None, + Some(|value: &str, row: &Map<String, Value>| { + let (copied, set_copied) = create_signal(false); + + let experiment_name = value.to_string(); + let experiment_id = row.get("id").map_or(String::from(""), |value| { + value.as_str().unwrap_or("").to_string() + }); + let experiment_id_copy = experiment_id.to_string(); + let handle_copy = move |event: MouseEvent| { + event.prevent_default(); + + let copy_code = + format!("navigator.clipboard.writeText({})", experiment_id_copy); + match js_sys::eval(©_code) { + Ok(_) => { + set_copied.set(true); + set_timeout( + move || { + set_copied.set(false); + }, + Duration::new(1, 0), + ); + } + Err(_) => logging::log!("unable to copy to clipboard"), + } + }; + view! { + <div> + <A href=experiment_id.to_string() class="btn-link"> + {experiment_name} + </A> + <div class="text-gray-500"> + <span class="text-xs"> + {experiment_id} + </span> + <i class="ri-file-copy-line cursor-pointer ml-2" on:click:undelegated=handle_copy></i> + <Show when=move || copied.get()> + <div class="inline-block bg-gray-600 ml-2 rounded-xl px-2"> + <span class="text-white text-xs font-semibold"> + "copied!" + </span> + </div> + </Show> + </div> + </div> + } + .into_view() + }), + ), + Column::new( + "status".to_string(), + None, + Some(|value: &str, _| { + let badge_color = match value { + "CREATED" => "badge-success", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-info", + &_ => "info", + }; + let class = format!("badge {}", badge_color); + view! { + <div class={class}> + <span class="text-white font-semibold text-xs"> + {value.to_string()} + </span> + </div> + } + .into_view() + }), + ), + Column::new( + "context".to_string(), + None, + Some(|_, row: &Map<String, Value>| { + let context = match row.get("context") { + Some(value) => value.to_owned(), + None => json!(""), + }; + + view! { + <div class="inline-flex flex-col gap-y-2"> + <ConditionPills context=context /> + </div> + } + .into_view() + }), + ), + Column::new( + "chosen_variant".to_string(), + None, + Some(|value: &str, _| { + let label = match value { + "null" => "".to_string(), + other => other.to_string(), + }; + + view! { + <span>{label}</span> + } + .into_view() + }), + ), + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), + Column::default("last_modified".to_string()), + ] +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 0dfa5c497..707cdd816 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -219,12 +219,14 @@ pub fn home() -> impl IntoView { item_one.dyn_ref::<HtmlSpanElement>().unwrap(), item_two.dyn_ref::<HtmlSpanElement>().unwrap(), ); - let _ = config_name_element.class_list().add_2("text-black", "font-bold"); + let _ = config_name_element + .class_list() + .add_2("text-black", "font-bold"); let _ = config_name_element.class_list().remove_1("text-gray-300"); - let _ = config_value_element.class_list().add_2("text-black", "font-bold"); let _ = config_value_element .class_list() - .remove_1("text-gray-300"); + .add_2("text-black", "font-bold"); + let _ = config_value_element.class_list().remove_1("text-gray-300"); logging::log!( "config name after replace {} and value {}", config_name_element.to_string(), @@ -286,9 +288,13 @@ pub fn home() -> impl IntoView { config_name_elements.item(i).unwrap(), config_value_elements.item(i).unwrap(), ); - let _ = config_name_element.class_list().remove_2("text-black", "font-bold"); + let _ = config_name_element + .class_list() + .remove_2("text-black", "font-bold"); let _ = config_name_element.class_list().add_1("text-gray-300"); - let _ = config_value_element.class_list().remove_2("text-black", "font-bold"); + let _ = config_value_element + .class_list() + .remove_2("text-black", "font-bold"); let _ = config_value_element.class_list().add_1("text-gray-300"); } logging::log!("query vector {:#?}", query_vector); From adafb493a85eff710178e2ae5e94952b57f3a238 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Tue, 19 Dec 2023 05:06:41 +0530 Subject: [PATCH 219/352] fix: frontend multi-tenancy support + config and dimension page --- .env.example | 4 +- crates/frontend/src/app.rs | 4 - crates/frontend/src/hoc/layout/layout.rs | 19 ++ .../pages/ContextOverride/ContextOverride.rs | 53 ++---- .../src/pages/DefaultConfig/DefaultConfig.rs | 154 +++++++++------- .../src/pages/Dimensions/Dimensions.rs | 165 ++++++++++++------ .../frontend/src/pages/Dimensions/helper.rs | 4 +- crates/frontend/src/pages/Experiment/mod.rs | 2 +- .../pages/ExperimentList/ExperimentList.rs | 6 +- .../src/pages/ExperimentList/utils.rs | 10 +- .../service-utils/src/middlewares/tenant.rs | 17 +- 11 files changed, 268 insertions(+), 170 deletions(-) diff --git a/.env.example b/.env.example index bb781fde4..efc2f4a0b 100644 --- a/.env.example +++ b/.env.example @@ -20,9 +20,9 @@ MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 DASHBOARD_AUTH_ENABLED=false -ENABLE_TENANT_AND_SCOPE=false +ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/,/default-config" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 380606670..f5379cb95 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -14,13 +14,9 @@ use crate::pages::{ pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); - let (tenant_rs, tenant_ws) = create_signal(String::from("mjos")); - provide_context(tenant_rs); - provide_context(tenant_ws); view! { <html data-theme="light"> <Stylesheet id="leptos" href="/pkg/style.css"/> - // <Link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/> <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> <Link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 73e19260d..e57a498ec 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -1,8 +1,27 @@ use crate::components::side_nav::side_nav::SideNav; use leptos::*; +use leptos_router::*; #[component] pub fn Layout(children: Children) -> impl IntoView { + let params = use_params_map(); + let location = use_location(); + + let tenant = match location + .pathname + .get() + .split("/") + .collect::<Vec<&str>>() + .get(2) + { + Some(s) => s.to_string(), + None => "mjos".to_string(), + }; + + let (tenant_rs, tenant_ws) = create_signal(tenant); + provide_context(tenant_rs); + provide_context(tenant_ws); + view! { <div> <SideNav/> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index a41e6880d..07a231446 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -33,18 +33,12 @@ fn ContextModalForm<NF>(handle_change: NF) -> impl IntoView where NF: Fn(Vec<(String, String, String)>) + 'static + Clone, { - let query = use_query_map(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let tenant = query.with(|params_map| { - params_map - .get("tenant") - .cloned() - .unwrap_or_else(|| "mjos".to_string()) - }); - let tenant = tenant.clone(); - - let dimensions = - create_blocking_resource(|| {}, move |_| fetch_dimensions(tenant.clone())); + let dimensions = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_dimensions(current_tenant.clone()), + ); view! { <div> @@ -163,18 +157,11 @@ fn OverrideModalForm<NF>(handle_change: NF) -> impl IntoView where NF: Fn(Map<String, Value>) + 'static + Clone, { - let query = use_query_map(); - - let tenant = query.with(|params_map| { - params_map - .get("tenant") - .cloned() - .unwrap_or_else(|| "mjos".to_string()) - }); - let tenant = tenant.clone(); - - let default_config = - create_blocking_resource(|| {}, move |_| fetch_default_config(tenant.clone())); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let default_config = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_default_config(current_tenant.clone()), + ); view! { <div> @@ -293,6 +280,8 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { create_signal::<Vec<(String, String, String)>>(vec![]); let (overrides, set_overrides) = create_signal::<Map<String, Value>>(Map::new()); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let handle_context_change = move |updated_ctx: Vec<(String, String, String)>| { set_context_condition.set(updated_ctx); }; @@ -305,6 +294,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let on_submit = { move |ev: MouseEvent| { let handle_submit_clone = handle_submit.clone(); + let current_tenant = tenant_rs.get(); ev.prevent_default(); logging::log!("tirggering submit"); @@ -313,7 +303,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let handle_submit = handle_submit_clone; async move { let result = create_context( - "mjos".to_string(), + current_tenant, overrides.get(), context_condition.get(), ) @@ -416,14 +406,7 @@ fn parse_conditions(input: String) -> Vec<(String, String, String)> { #[component] pub fn ContextOverride() -> impl IntoView { - let query = use_query_map(); - - let tenant = query.with(|params_map| { - params_map - .get("tenant") - .cloned() - .unwrap_or_else(|| "mjos".to_string()) - }); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let context_data: Vec<(String, String, String)> = vec![]; let ctx: RwSignal<Vec<(String, String, String)>> = create_rw_signal(context_data); @@ -436,8 +419,10 @@ pub fn ContextOverride() -> impl IntoView { provide_context(ovr_data); - let config_data = - create_blocking_resource(|| {}, move |_| fetch_config(tenant.clone())); + let config_data = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_config(current_tenant.clone()), + ); let table_columns = create_memo(move |_| { vec![ diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 3db5df04e..207dcf2b1 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -9,11 +9,11 @@ use crate::pages::DefaultConfig::types::Config; use crate::pages::ExperimentList::utils::fetch_default_config; use crate::utils::modal_action; use js_sys; -use leptos::ev::SubmitEvent; use leptos::spawn_local; use leptos::*; -use leptos_router::use_query_map; +use reqwest::StatusCode; use serde_json::{json, Map, Value}; +use web_sys::MouseEvent; #[derive(Clone, Debug, Default)] pub struct RowData { @@ -97,7 +97,12 @@ pub async fn create_default_config( .send() .await .map_err(|e| e.to_string())?; - response.text().await.map_err(|e| e.to_string()) + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } } #[component] @@ -130,14 +135,20 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let (pattern, set_pattern) = create_signal("".to_string()); let edit_signal = use_context::<RwSignal<bool>>(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (show_labels, set_show_labels) = create_signal(false); create_effect(move |_| { if let Some(row_data) = global_state { logging::log!("default config create effect"); - set_key.set(row_data.get().key.clone().to_string()); - set_value.set(row_data.get().value.clone().to_string()); + if edit_signal.unwrap().get() == true { + set_key.set(row_data.get().key.clone().to_string()); + set_value.set(row_data.get().value.clone().to_string()); + } else { + set_key.set("".to_string()); + set_value.set("".to_string()); + } } }); @@ -145,11 +156,14 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let input_element_two: NodeRef<Input> = create_node_ref(); let input_element_three: NodeRef<Textarea> = create_node_ref(); + let (error_message, set_error_message) = create_signal("".to_string()); + let on_submit = { let handle_submit = handle_submit.clone(); - move |ev: SubmitEvent| { + move |ev: MouseEvent| { ev.prevent_default(); + let current_tenant = tenant_rs.get(); let value1 = input_element.get().expect("<input> to exist").value(); let value2 = input_element_two.get().expect("<input> to exist").value(); let value3 = input_element_three.get().expect("<input> to exist").value(); @@ -164,7 +178,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let handle_submit = handle_submit_clone; async move { let result = create_default_config( - "mjos".to_string(), + current_tenant, key.get(), value.get(), keytype.get(), @@ -175,8 +189,10 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { match result { Ok(_) => { handle_submit(); + modal_action("my_modal_5", "close"); } - Err(_) => { + Err(e) => { + set_error_message.set(e); // Handle error // Consider logging or displaying the error } @@ -189,7 +205,6 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { view! { <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - on:submit=on_submit > <div class="form-control"> <label class="label font-mono"> @@ -206,43 +221,61 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { </div> - <div tabindex = "0" class="dropdown dropdown-bottom"> - <div tabindex="0" role="button" class="btn m-1"> - <i class="ri-arrow-down-s-line"></i> - Add Schema - </div> + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_keytype.set("number".to_string()); + } + "Enum" => { + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "Pattern" => { + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + } + _ => { + set_keytype.set("Other".to_string()); + set_pattern.set("".to_string()); + } + }; + + } + class="select select-bordered" + > + <option disabled selected> + Set Schema + </option> + + <option + value= "number" + selected=move || {keytype.get() == "number".to_string()} + > + "Number" + </option> + <option + value= "Enum" + selected=move || { keytype.get() == "Enum".to_string()} + > + "String (Enum)" + </option> + <option + value= "Pattern" + selected=move || { keytype.get() == "Pattern".to_string()} + > + "String (regex)" + </option> + <option + value= "Other" + selected=move || { keytype.get() == "Other".to_string()} + > + "Other" + </option> + </select> - <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 - rounded-box w-52"> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("number".to_string()); - }> - <a>"Number"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - }> - <a>"String (Enum)"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - }> - <a>"String (Regex)"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Other".to_string()); - set_pattern.set("".to_string()); - }> - <a>"Other"</a> - </li> - </ul> - </div> { move || { view!{ @@ -292,8 +325,17 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { } } <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> + <Button text="Submit".to_string() on_click= on_submit /> </div> + + { + + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } </form> } } @@ -315,7 +357,7 @@ fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { }; let edit_icon: HtmlElement<html::I> = - view! { <i class="ri-pencil-line ri-xl text-blue-500 cursor-pointer"></i> }; + view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; view! { <span on:click = edit_click_handler>{edit_icon}</span> }.into_view() } @@ -329,19 +371,11 @@ pub fn DefaultConfig() -> impl IntoView { let edit_signal = create_rw_signal(true); provide_context(edit_signal); - let query = use_query_map(); - - let tenant = query.with(|params_map| { - params_map - .get("tenant") - .cloned() - .unwrap_or_else(|| "mjos".to_string()) - }); - + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let default_config_resource = create_blocking_resource( - || (), - |_| async move { - match fetch_default_config().await { + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_default_config(¤t_tenant).await { Ok(data) => data, Err(_) => vec![], } @@ -420,4 +454,4 @@ pub fn DefaultConfig() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 3aa30fcb0..334a2f9f7 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -4,16 +4,14 @@ use std::rc::Rc; use crate::components::Button::Button::Button; use crate::components::{ stat::stat::Stat, - table::{ - table::Table, - types::{Column, TableSettings}, - }, + table::{table::Table, types::Column}, }; use crate::utils::modal_action; use leptos::logging::*; use leptos::*; +use reqwest::StatusCode; use serde_json::{json, Map, Value}; -use web_sys::SubmitEvent; +use web_sys::MouseEvent; use crate::pages::Dimensions::helper::fetch_dimensions; @@ -27,12 +25,15 @@ pub struct RowData { fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { // Parse the input string into a serde_json::Value - let parsed = serde_json::from_str::<Value>(input).expect("Failed to parse JSON"); + let parsed = serde_json::from_str::<Value>(input); // Ensure the Value is an Array and convert it to Vec<Value> match parsed { - Value::Array(arr) => arr, - _ => panic!("Input is not a JSON array"), + Ok(Value::Array(arr)) => arr, + _ => { + logging::log!("Not a valid json in the input"); + vec![] + } } } @@ -115,7 +116,12 @@ pub async fn create_dimension( .send() .await .map_err(|e| e.to_string())?; - response.text().await.map_err(|e| e.to_string()) + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } } #[component] @@ -151,20 +157,28 @@ fn FormComponent( create_effect(move |_| { if let Some(row_data) = global_state { logging::log!("default config create effect"); - set_dimension.set(row_data.get().dimension.clone().to_string()); - set_priority.set(row_data.get().priority.clone()); - set_keytype.set(row_data.get().type_.clone().to_string()); - set_pattern.set(row_data.get().pattern.clone()); + if edit_signal.unwrap().get() == true { + set_dimension.set(row_data.get().dimension.clone().to_string()); + set_priority.set(row_data.get().priority.clone()); + set_keytype.set(row_data.get().type_.clone().to_string()); + set_pattern.set(row_data.get().pattern.clone()); + } else { + set_dimension.set("".to_string()); + set_priority.set("".to_string()); + set_keytype.set("".to_string()); + set_pattern.set("".to_string()); + } } }); let input_element: NodeRef<Input> = create_node_ref(); let input_element_two: NodeRef<Input> = create_node_ref(); let input_element_three: NodeRef<Textarea> = create_node_ref(); + let (error_message, set_error_message) = create_signal("".to_string()); let on_submit = { let handle_submit = handle_submit.clone(); - move |ev: SubmitEvent| { + move |ev: MouseEvent| { ev.prevent_default(); let value1 = input_element.get().expect("<input> to exist").value(); @@ -191,8 +205,10 @@ fn FormComponent( match result { Ok(_) => { handle_submit(); + // modal_action("my_modal_5","close"); } - Err(_) => { + Err(e) => { + set_error_message.set(e); // Handle error // Consider logging or displaying the error } @@ -212,7 +228,6 @@ fn FormComponent( </form> <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - on:submit=on_submit > <div class="form-control"> <label class="label font-mono"> @@ -227,42 +242,60 @@ fn FormComponent( node_ref=input_element /> </div> - <div tabindex = "0" class="dropdown dropdown-bottom"> - <div tabindex="0" role="button" class="btn m-1"> - <i class="ri-arrow-down-s-line"></i> - Add Schema - </div> - - <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 - rounded-box w-52"> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("number".to_string()); - }> - <a>"Number"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - }> - <a>"String (Enum)"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - }> - <a>"String (Regex)"</a> - </li> - <li on:click = move |_| { - set_show_labels.set(true); - set_keytype.set("Other".to_string()); - }> - <a>"Other"</a> - </li> - </ul> - </div> + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_keytype.set("number".to_string()); + } + "Enum" => { + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "Pattern" => { + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + } + _ => { + set_keytype.set("Other".to_string()); + set_pattern.set("".to_string()); + } + }; + + } + class="select select-bordered" + > + <option disabled selected> + Set Schema + </option> + + <option + value= "number" + selected=move || {keytype.get() == "number".to_string()} + > + "Number" + </option> + <option + value= "Enum" + selected=move || { keytype.get() == "Enum".to_string()} + > + "String (Enum)" + </option> + <option + value= "Pattern" + selected=move || { keytype.get() == "Pattern".to_string()} + > + "String (regex)" + </option> + <option + value= "Other" + selected=move || { keytype.get() == "Other".to_string()} + > + "Other" + </option> + </select> { move || { view!{ @@ -312,8 +345,16 @@ fn FormComponent( } } <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= |_| modal_action("my_modal_5","close") /> + <Button text="Submit".to_string() on_click= on_submit /> </div> + { + + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } </form> </div> </dialog> @@ -328,11 +369,12 @@ pub fn Dimensions() -> impl IntoView { let edit_signal = create_rw_signal(true); provide_context(edit_signal); + let (open_form, set_open_form) = create_signal(false); let dimensions = create_blocking_resource( - move || {}, - |_value| async move { - match fetch_dimensions().await { + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_dimensions(¤t_tenant).await { Ok(data) => data, Err(_) => vec![], } @@ -369,10 +411,16 @@ pub fn Dimensions() -> impl IntoView { /> } }} + <Show when = move || { open_form.get() }> <ModalComponent - handle_submit=Rc::new(move || dimensions.refetch()) + handle_submit=Rc::new(move || { + set_open_form.set(false); + dimensions.refetch() + } + ) tenant=tenant_rs /> + </Show> </div> <div class="card rounded-xl w-full bg-base-100 shadow"> @@ -383,7 +431,8 @@ pub fn Dimensions() -> impl IntoView { let edit_clone = edit_signal.to_owned(); move |_| { edit_clone.set(false); - modal_action("my_modal_5","open") + set_open_form.set(true); + modal_action("my_modal_5","open"); }}/> </div> <div> diff --git a/crates/frontend/src/pages/Dimensions/helper.rs b/crates/frontend/src/pages/Dimensions/helper.rs index c5f32c7d2..ca5e32411 100644 --- a/crates/frontend/src/pages/Dimensions/helper.rs +++ b/crates/frontend/src/pages/Dimensions/helper.rs @@ -2,7 +2,7 @@ use std::vec::Vec; use super::types::Dimension; -pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { +pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { Ok("PROD") => { @@ -15,7 +15,7 @@ pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { let url = format!("{}/dimension", host); let response: Vec<Dimension> = client .get(url) - .header("x-tenant", "mjos") + .header("x-tenant", tenant) .send() .await .map_err(|e| e.to_string())? diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 0a562ba4b..623ffbf31 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -515,4 +515,4 @@ fn add_dialogs( }.into_view(), ExperimentStatusType::CONCLUDED => view! {}.into_view(), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 2adfadf59..3f0f8b13c 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -50,8 +50,8 @@ pub fn ExperimentList() -> impl IntoView { |(current_tenant, filters)| async move { // Perform all fetch operations concurrently let experiments_future = fetch_experiments(filters, ¤t_tenant); - let dimensions_future = fetch_dimensions(); - let config_future = fetch_default_config(); + let dimensions_future = fetch_dimensions(¤t_tenant); + let config_future = fetch_default_config(¤t_tenant); let (experiments_result, dimensions_result, config_result) = join!(experiments_future, dimensions_future, config_future); @@ -294,4 +294,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 110535942..08721f694 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -54,7 +54,7 @@ pub async fn fetch_experiments( Ok(response) } -pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { +pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { Ok("PROD") => { @@ -67,7 +67,7 @@ pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { let url = format!("{}/dimension", host); let response: Vec<Dimension> = client .get(url) - .header("x-tenant", "mjos") + .header("x-tenant", tenant) .send() .await .map_err(|e| e.to_string())? @@ -78,7 +78,7 @@ pub async fn fetch_dimensions() -> Result<Vec<Dimension>, String> { Ok(response) } -pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { +pub async fn fetch_default_config(tenant: &str) -> Result<Vec<DefaultConfig>, String> { let client = reqwest::Client::new(); let host = match std::env::var("APP_ENV").as_deref() { Ok("PROD") => { @@ -91,7 +91,7 @@ pub async fn fetch_default_config() -> Result<Vec<DefaultConfig>, String> { let url = format!("{}/default-config", host); let response: Vec<DefaultConfig> = client .get(url) - .header("x-tenant", "mjos") + .header("x-tenant", tenant) .send() .await .map_err(|e| e.to_string())? @@ -213,4 +213,4 @@ pub fn experiment_table_columns() -> Vec<Column> { Column::default("created_by".to_string()), Column::default("last_modified".to_string()), ] -} \ No newline at end of file +} diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index 86e6e89b6..b57254b51 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -59,6 +59,21 @@ where } }; + let path = req.path(); + let tenant_from_params = match req.match_pattern() { + Some(pattern) => { + let pattern_segments = pattern.split("/"); + let path_segments = path.split("/").collect::<Vec<&str>>(); + Some( + pattern_segments + .enumerate() + .find(|(_, segment)| segment == &"{tenant}") + .map_or("", |(idx, _)| path_segments[idx]), + ) + } + None => None, + }; + let request_path = req.uri().path(); let is_excluded: bool = app_state .tenant_middleware_exclusion_list @@ -68,7 +83,7 @@ where let tenant = req .headers() .get("x-tenant") - .map_or(None, |header_value: &HeaderValue| { + .map_or(tenant_from_params, |header_value: &HeaderValue| { header_value.to_str().ok() }) .map(|header_str| header_str.to_string()); From caa97393d3419b79661aa1d44047c5d861d45061 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Tue, 19 Dec 2023 14:23:16 +0530 Subject: [PATCH 220/352] fix: UI fixes for demo --- crates/frontend/src/components/condition_pills/utils.rs | 4 ++-- crates/frontend/src/pages/ContextOverride/ContextOverride.rs | 1 - crates/frontend/src/pages/Home/Home.rs | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs index c7a61bbb6..9f721eb7d 100644 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -14,7 +14,7 @@ pub fn extract_and_format(condition: &Value) -> String { formatted_conditions.push(format_condition(cond)); } - formatted_conditions.join(" and ") + formatted_conditions.join(" && ") } else { // Handling single conditions format_condition(condition) @@ -71,7 +71,7 @@ pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { let operators = vec!["==", "in"]; // Split the string by "and" and iterate over each condition - for condition in input.split("and") { + for condition in input.split("&&") { let mut parts = Vec::new(); let mut operator_found = ""; diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 07a231446..4e221cfdd 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -10,7 +10,6 @@ use crate::pages::DefaultConfig::types::Config; use crate::components::condition_pills::condition_pills::ConditionPills; use crate::utils::modal_action; use leptos::*; -use leptos_router::use_query_map; use reqwest::StatusCode; use serde_json::{json, Map, Value}; use web_sys::MouseEvent; diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 707cdd816..0e7756ed0 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -199,7 +199,9 @@ pub fn home() -> impl IntoView { let search_field_prefix = gen_name_id( search_field_prefix, dimension, - &value.as_str().unwrap().to_string(), + &value.as_str().unwrap_or( + &value.to_string().trim_matches('"')[..] + ).to_string(), ); logging::log!("search field prefix {:#?}", search_field_prefix); let config_name_elements = document() From 4db1f16d9c698dced8b8a79867ccda0503ce29e5 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 20 Dec 2023 14:53:29 +0530 Subject: [PATCH 221/352] chore: formatted frontend code --- .../frontend/src/components/Button/Button.rs | 4 +- crates/frontend/src/components/Button/mod.rs | 2 +- .../condition_pills/condition_pills.rs | 4 +- .../components/context_form/context_form.rs | 8 +- .../src/components/context_form/utils.rs | 4 +- .../experiment_form/experiment_form.rs | 156 +++++----- .../src/components/experiment_form/types.rs | 6 +- .../src/components/experiment_form/utils.rs | 4 +- crates/frontend/src/components/mod.rs | 4 +- .../src/components/side_nav/side_nav.rs | 5 +- crates/frontend/src/components/table/table.rs | 8 +- crates/frontend/src/hoc/layout/layout.rs | 3 +- .../pages/ContextOverride/ContextOverride.rs | 16 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 251 ++++++++-------- .../src/pages/Dimensions/Dimensions.rs | 281 +++++++++--------- crates/frontend/src/pages/Dimensions/types.rs | 7 +- crates/frontend/src/pages/Experiment/mod.rs | 42 +-- .../pages/ExperimentList/ExperimentList.rs | 23 +- .../src/pages/ExperimentList/types.rs | 4 +- crates/frontend/src/pages/Home/Home.rs | 73 +++-- 20 files changed, 466 insertions(+), 439 deletions(-) diff --git a/crates/frontend/src/components/Button/Button.rs b/crates/frontend/src/components/Button/Button.rs index ba5072593..b1ac6d75c 100644 --- a/crates/frontend/src/components/Button/Button.rs +++ b/crates/frontend/src/components/Button/Button.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use leptos::*; use web_sys::MouseEvent; @@ -14,4 +12,4 @@ pub fn Button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl In <i class="ri-edit-2-line ml-2"></i> </button> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/Button/mod.rs b/crates/frontend/src/components/Button/mod.rs index 62044bd9b..8ddf4525c 100644 --- a/crates/frontend/src/components/Button/mod.rs +++ b/crates/frontend/src/components/Button/mod.rs @@ -1 +1 @@ -pub mod Button; +pub mod button; \ No newline at end of file diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs index fd99ad75f..3603e1cee 100644 --- a/crates/frontend/src/components/condition_pills/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -19,9 +19,7 @@ pub fn ConditionPills(context: Value) -> impl IntoView { <span class="font-mono font-medium text-gray-650 context_condition "> {op} </span> - <span class="font-mono font-semibold context_condition"> - {val} - </span> + <span class="font-mono font-semibold context_condition">{val}</span> </span> } }) diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index c8dd5e1bd..ce0f427fa 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -14,8 +14,6 @@ pub fn ContextForm<NF>( where NF: Fn(Vec<(String, String, String)>) + 'static, { - let has_dimensions = dimensions.len() > 0; - let (context, set_context) = create_signal(context.clone()); let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); @@ -148,7 +146,9 @@ where </div> <div class="form-control"> <label class="label font-mono text-sm"> - <span class="label-text" name="context-dimension-name">{dimension_label}</span> + <span class="label-text" name="context-dimension-name"> + {dimension_label} + </span> </label> <div class="flex gap-x-6 items-center"> <input @@ -213,4 +213,4 @@ where </Show> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 18021d731..52def6f55 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,4 +1,4 @@ -use reqwest::{Error, StatusCode}; +use reqwest::StatusCode; use serde_json::{json, Map, Value}; pub fn construct_context(conditions: Vec<(String, String, String)>) -> Value { @@ -72,4 +72,4 @@ pub async fn create_context( StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), _ => Err("Internal Server Error".to_string()), } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 8698736b7..e859ea8c7 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,6 +1,5 @@ use super::utils::create_experiment; -use crate::api::{fetch_default_config, fetch_dimensions}; -use crate::components::Button::Button::Button; +use crate::components::button::button::Button; use crate::components::{ context_form::context_form::ContextForm, override_form::override_form::OverrideForm, }; @@ -8,7 +7,6 @@ use crate::pages::ExperimentList::types::{ DefaultConfig, Dimension, Variant, VariantType, }; use chrono::offset::Local; -use futures::join; use leptos::*; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -53,7 +51,6 @@ pub fn ExperimentForm<NF>( handle_submit: NF, default_config: Vec<DefaultConfig>, dimensions: Vec<Dimension>, - edit_mode: bool, ) -> impl IntoView where NF: Fn() + 'static + Clone, @@ -172,99 +169,100 @@ where context_id: None, override_id: None, overrides: Map::new(), - } + }, )) }); } > + <i class="ri-add-line"></i> Add Variant </button> </div> - <For - each=move || { - f_variants - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, (String, Variant))>>() - } - key=|(_, (key, _))| key.to_string() - children=move |(idx, (_, variant))| { - let variant_clone = variant.clone(); - let default_config = default_config.clone(); - let handle_change = handle_override_form_change(idx); + <For + each=move || { + f_variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, Variant))>>() + } - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=variant.variant_type.to_string() - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants.update(|value| { + key=|(_, (key, _))| key.to_string() + children=move |(idx, (_, variant))| { + let variant_clone = variant.clone(); + let default_config = default_config.clone(); + let handle_change = handle_override_form_change(idx); + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center gap-4"> + <div class="form-control w-1/3"> + <label class="label"> + <span class="label-text">ID</span> + </label> + <input + name="variantId" + value=move || variant.id.to_string() + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs" + /> + </div> + <div class="form-control w-1/3"> + <label class="label font-medium text-sm"> + <span class="label-text">Type</span> + </label> + <select + name="expType[]" + value=variant.variant_type.to_string() + on:change=move |ev| { + let mut new_variant = variant_clone.clone(); + new_variant + .variant_type = match event_target_value(&ev).as_str() { + "CONTROL" => VariantType::CONTROL, + _ => VariantType::EXPERIMENTAL, + }; + set_variants + .update(|value| { value[idx].1 = new_variant; }) - } + } - class="select select-bordered" + class="select select-bordered" + > + <option disabled>Pick one</option> + <option + value=VariantType::CONTROL.to_string() + selected=VariantType::CONTROL == variant.variant_type > - <option disabled> - Pick one - </option> - <option - value=VariantType::CONTROL.to_string() - selected={VariantType::CONTROL == variant.variant_type} - > - {VariantType::CONTROL.to_string()} - </option> - <option - value=VariantType::EXPERIMENTAL.to_string() - selected={VariantType::EXPERIMENTAL == variant.variant_type} - > - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> - </div> - </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=default_config - handle_change=handle_change - is_standalone=false - /> + {VariantType::CONTROL.to_string()} + </option> + <option + value=VariantType::EXPERIMENTAL.to_string() + selected=VariantType::EXPERIMENTAL == variant.variant_type + > + {VariantType::EXPERIMENTAL.to_string()} + </option> + </select> </div> </div> - } + <div class="mt-2"> + <OverrideForm + overrides=variant.overrides + default_config=default_config + handle_change=handle_change + is_standalone=false + /> + </div> + </div> } - /> + } + /> + </div> <div class="flex justify-end mt-8"> - <Button text="Submit".to_string() on_click = on_submit /> + <Button text="Submit".to_string() on_click=on_submit/> </div> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs index 0ae50e734..2aeb05610 100644 --- a/crates/frontend/src/components/experiment_form/types.rs +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -1,6 +1,4 @@ -use crate::pages::ExperimentList::types::{ - DefaultConfig, Dimension, Variant, VariantType, -}; +use crate::pages::ExperimentList::types::Variant; use serde::Serialize; use serde_json::Value; @@ -10,4 +8,4 @@ pub struct ExperimentCreateRequest { pub context: Value, pub variants: Vec<Variant>, -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index 8e2406e69..eafc905ab 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -2,7 +2,7 @@ use super::types::ExperimentCreateRequest; use crate::components::context_form::utils::construct_context; use crate::pages::ExperimentList::types::Variant; use reqwest::StatusCode; -use serde_json::{json, Value}; +use serde_json::json; pub async fn create_experiment( conditions: Vec<(String, String, String)>, @@ -33,4 +33,4 @@ pub async fn create_experiment( StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), _ => Err("Internal Server Error".to_string()), } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index d0d257a37..889511971 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,4 +1,4 @@ -pub mod Button; +pub mod button; pub mod condition_pills; pub mod context_form; pub mod experiment_form; @@ -7,4 +7,4 @@ pub mod override_form; pub mod pagination; pub mod side_nav; pub mod stat; -pub mod table; +pub mod table; \ No newline at end of file diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index fa912e3d8..07f58e33a 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -34,7 +34,7 @@ pub fn SideNav() -> impl IntoView { for tenant in tenants.into_iter() { if tenant == tenant_rs.get() { view_vector - .push(view! { <option selected={true}>{tenant}</option> }.into_view()); + .push(view! { <option selected=true>{tenant}</option> }.into_view()); } else { view_vector.push(view! { <option>{tenant}</option> }.into_view()); } @@ -88,9 +88,10 @@ pub fn SideNav() -> impl IntoView { Default::default(), ); } + class="select w-full max-w-xs shadow-md" > - { view_vector } + {view_vector} </select> // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index 373adecfa..ec46defea 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -34,7 +34,11 @@ pub fn Table( {columns .iter() .filter(|column| !column.hidden) - .map(|column| view! { <th class="uppercase">{&column.name.replace("_", " ")}</th> }) + .map(|column| { + view! { + <th class="uppercase">{&column.name.replace("_", " ")}</th> + } + }) .collect_view()} </tr> @@ -52,7 +56,7 @@ pub fn Table( .unwrap() .to_string(); view! { - <tr id={row_id}> + <tr id=row_id> <th>{index + 1}</th> {columns diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index e57a498ec..857d25dca 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -4,7 +4,6 @@ use leptos_router::*; #[component] pub fn Layout(children: Children) -> impl IntoView { - let params = use_params_map(); let location = use_location(); let tenant = match location @@ -30,4 +29,4 @@ pub fn Layout(children: Children) -> impl IntoView { </main> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 4e221cfdd..f2f47f9be 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -4,7 +4,7 @@ use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; -use crate::components::Button::Button::Button; +use crate::components::button::button::Button; use crate::pages::DefaultConfig::types::Config; // use leptos::spawn_local; use crate::components::condition_pills::condition_pills::ConditionPills; @@ -309,7 +309,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { .await; match result { - Ok(str) => { + Ok(_) => { handle_submit(); modal_action("my_modal_5", "close") } @@ -483,7 +483,7 @@ pub fn ContextOverride() -> impl IntoView { "Condition" </h3> <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> - <ConditionPills context={context.condition.clone()} /> + <ConditionPills context=context.condition.clone()/> </div> <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> <i class="ri-edit-line text-blue-500"></i> @@ -515,13 +515,7 @@ pub fn ContextOverride() -> impl IntoView { }, ] } - None => { - vec![ - view! { - <div>Loading....</div> - }, - ] - } + None => vec![view! { <div>Loading....</div> }], } }) }} @@ -530,4 +524,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 207dcf2b1..e5dca3057 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use crate::components::table::{table::Table, types::Column}; use crate::components::stat::stat::Stat; -use crate::components::Button::Button::Button; +use crate::components::button::button::Button; use crate::pages::DefaultConfig::types::Config; use crate::pages::ExperimentList::utils::fetch_default_config; use crate::utils::modal_action; @@ -127,7 +127,7 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { use leptos::html::Textarea; let handle_submit = handle_submit.clone(); let global_state = use_context::<RwSignal<RowData>>(); - let row_data = global_state.unwrap().get(); + let _row_data = global_state.unwrap().get(); let (key, set_key) = create_signal("".to_string()); let (value, set_value) = create_signal("".to_string()); @@ -203,15 +203,13 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { }; view! { - <form - class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - > + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Key</span> </label> <input - disabled = move || {edit_signal.unwrap().get()} + disabled=move || { edit_signal.unwrap().get() } type="text" placeholder="Key" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -220,123 +218,112 @@ fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { /> </div> - <select - name="schemaType[]" - on:change=move |ev| { - set_show_labels.set(true); - match event_target_value(&ev).as_str() { - "number" => { - set_keytype.set("number".to_string()); - } - "Enum" => { - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - } - "Pattern" => { - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - } - _ => { - set_keytype.set("Other".to_string()); - set_pattern.set("".to_string()); - } - }; + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_keytype.set("number".to_string()); + } + "Enum" => { + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "Pattern" => { + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + } + _ => { + set_keytype.set("Other".to_string()); + set_pattern.set("".to_string()); + } + }; + } - } - class="select select-bordered" + class="select select-bordered" > - <option disabled selected> - Set Schema - </option> - - <option - value= "number" - selected=move || {keytype.get() == "number".to_string()} - > - "Number" - </option> - <option - value= "Enum" - selected=move || { keytype.get() == "Enum".to_string()} - > - "String (Enum)" - </option> - <option - value= "Pattern" - selected=move || { keytype.get() == "Pattern".to_string()} - > - "String (regex)" - </option> - <option - value= "Other" - selected=move || { keytype.get() == "Other".to_string()} - > - "Other" - </option> - </select> - - { - move || { - view!{ - <Show when = move || (keytype.get() == "number")> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> - </label> - <input - type="number" - placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=value - node_ref=input_element_two - /> - </div> - </Show> - - <Show when = move || (show_labels.get() && (keytype.get() != "number")) > - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> - </label> - <input - type="text" - placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=value - node_ref=input_element_two - /> - </div> + <option disabled selected> + Set Schema + </option> + + <option value="number" selected=move || { keytype.get() == "number".to_string() }> + "Number" + </option> + <option value="Enum" selected=move || { keytype.get() == "Enum".to_string() }> + "String (Enum)" + </option> + <option value="Pattern" selected=move || { keytype.get() == "Pattern".to_string() }> + "String (regex)" + </option> + <option value="Other" selected=move || { keytype.get() == "Other".to_string() }> + "Other" + </option> + </select> + + {move || { + view! { + <Show when=move || (keytype.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="number" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=value + node_ref=input_element_two + /> + </div> + </Show> + + <Show when=move || (show_labels.get() && (keytype.get() != "number"))> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="text" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=value + node_ref=input_element_two + /> + </div> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">{keytype.get()}</span> - </label> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {keytype.get()} + </span> + </label> <textarea - type = "text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + type="text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" - node_ref=input_element_three - > {pattern.get()} + node_ref=input_element_three + > + {pattern.get()} </textarea> + </div> + </Show> + } + }} + + <div class="form-control mt-6"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + { + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> </div> - </Show> } - } } - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= on_submit /> - </div> - - { - view! { - <div> - <p class="text-red-500">{move || error_message.get()}</p> - </div> - } - } - </form> + </form> } } @@ -359,7 +346,7 @@ fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; - view! { <span on:click = edit_click_handler>{edit_icon}</span> }.into_view() + view! { <span on:click=edit_click_handler>{edit_icon}</span> }.into_view() } #[component] @@ -396,35 +383,34 @@ pub fn DefaultConfig() -> impl IntoView { view! { <div class="p-8"> <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> - <Suspense - fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - } - > - { + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + { let edit_signal = edit_signal.clone(); move || { let default_config = default_config_resource.get().unwrap_or(vec![]); let total_default_config_keys = default_config.len().to_string(); - let edit_signal = edit_signal.clone(); - let table_rows = default_config .into_iter() .map(|config| { let mut ele_map = json!(config).as_object().unwrap().to_owned(); - ele_map.insert("created_at".to_string(),json!(config.created_at.format("%v").to_string())); + ele_map + .insert( + "created_at".to_string(), + json!(config.created_at.format("%v").to_string()), + ); ele_map }) .collect::<Vec<Map<String, Value>>>(); - view! { <div class="pb-4"> <Stat heading="Config Keys" icon="ri-tools-line" - number={total_default_config_keys} + number=total_default_config_keys /> </div> <div class="card rounded-lg w-full bg-base-100 shadow"> @@ -433,12 +419,16 @@ pub fn DefaultConfig() -> impl IntoView { <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> "Default Config" </h2> - <Button text="Create Key".to_string() on_click= { - let edit_signal_clone = edit_signal.to_owned(); - move |_| { - edit_signal_clone.set(false); - modal_action("my_modal_5","open"); - }}/> + <Button + text="Create Key".to_string() + on_click={ + let edit_signal_clone = edit_signal.to_owned(); + move |_| { + edit_signal_clone.set(false); + modal_action("my_modal_5", "open"); + } + } + /> </div> <Table table_style="font-mono".to_string() @@ -451,7 +441,8 @@ pub fn DefaultConfig() -> impl IntoView { } } } + </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 334a2f9f7..35ef16abe 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,13 +1,12 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::components::Button::Button::Button; +use crate::components::button::button::Button; use crate::components::{ stat::stat::Stat, table::{table::Table, types::Column}, }; use crate::utils::modal_action; -use leptos::logging::*; use leptos::*; use reqwest::StatusCode; use serde_json::{json, Map, Value}; @@ -226,15 +225,13 @@ fn FormComponent( <i class="ri-close-fill" onclick="my_modal_5.close()"></i> </button> </form> - <form - class="form-control w-full space-y-4 bg-white text-gray-700 font-mono" - > + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Dimension</span> </label> <input - disabled = move || {edit_signal.unwrap().get()} + disabled=move || { edit_signal.unwrap().get() } type="text" placeholder="Dimension" class="input input-bordered w-full bg-white text-gray-700 shadow-md" @@ -243,118 +240,126 @@ fn FormComponent( /> </div> <select - name="schemaType[]" - on:change=move |ev| { - set_show_labels.set(true); - match event_target_value(&ev).as_str() { - "number" => { - set_keytype.set("number".to_string()); - } - "Enum" => { - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - } - "Pattern" => { - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - } - _ => { - set_keytype.set("Other".to_string()); - set_pattern.set("".to_string()); - } - }; + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_keytype.set("number".to_string()); + } + "Enum" => { + set_keytype.set("Enum".to_string()); + set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "Pattern" => { + set_keytype.set("Pattern".to_string()); + set_pattern.set(".*".to_string()); + } + _ => { + set_keytype.set("Other".to_string()); + set_pattern.set("".to_string()); + } + }; + } - } - class="select select-bordered" - > - <option disabled selected> - Set Schema - </option> - - <option - value= "number" - selected=move || {keytype.get() == "number".to_string()} - > - "Number" - </option> - <option - value= "Enum" - selected=move || { keytype.get() == "Enum".to_string()} - > - "String (Enum)" - </option> - <option - value= "Pattern" - selected=move || { keytype.get() == "Pattern".to_string()} - > - "String (regex)" - </option> - <option - value= "Other" - selected=move || { keytype.get() == "Other".to_string()} - > - "Other" - </option> - </select> - { - move || { - view!{ - <Show when = move || (keytype.get() == "number")> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> - </Show> - - <Show when = move || (show_labels.get() && (keytype.get() != "number")) > - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> + class="select select-bordered" + > + <option disabled selected> + Set Schema + </option> + + <option + value="number" + selected=move || { keytype.get() == "number".to_string() } + > + "Number" + </option> + <option + value="Enum" + selected=move || { keytype.get() == "Enum".to_string() } + > + "String (Enum)" + </option> + <option + value="Pattern" + selected=move || { keytype.get() == "Pattern".to_string() } + > + "String (regex)" + </option> + <option + value="Other" + selected=move || { keytype.get() == "Other".to_string() } + > + "Other" + </option> + </select> + + {move || { + view! { + <Show when=move || (keytype.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + Priority + </span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + </Show> + + <Show when=move || (show_labels.get() && (keytype.get() != "number"))> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">{keytype.get()}</span> - </label> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + Priority + </span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority + node_ref=input_element_two + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {keytype.get()} + </span> + </label> <textarea - type = "text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + type="text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" - node_ref=input_element_three - > {pattern.get()} + node_ref=input_element_three + > + {pattern.get()} </textarea> - </div> - </Show> + </div> + </Show> } - } - } + }} + <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click= on_submit /> + <Button text="Submit".to_string() on_click=on_submit/> </div> - { + { view! { - <div> - <p class="text-red-500">{move || error_message.get()}</p> - </div> - } - } + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } + </form> </div> </dialog> @@ -396,30 +401,26 @@ pub fn Dimensions() -> impl IntoView { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - { - move || { + + {move || { let value = dimensions.get(); let total_items = match value { Some(v) => v.len().to_string(), _ => "0".to_string(), }; view! { - <Stat - heading="Dimensions" - icon="ri-ruler-2-fill" - number={total_items} - /> + <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> } }} - <Show when = move || { open_form.get() }> - <ModalComponent - handle_submit=Rc::new(move || { - set_open_form.set(false); - dimensions.refetch() - } - ) - tenant=tenant_rs - /> + <Show when=move || { open_form.get() }> + <ModalComponent + handle_submit=Rc::new(move || { + set_open_form.set(false); + dimensions.refetch() + }) + + tenant=tenant_rs + /> </Show> </div> @@ -427,18 +428,21 @@ pub fn Dimensions() -> impl IntoView { <div class="card-body"> <div class="flex justify-between mb-2"> <h2 class="card-title">Dimensions</h2> - <Button text="Create Dimension".to_string() on_click={ - let edit_clone = edit_signal.to_owned(); - move |_| { - edit_clone.set(false); - set_open_form.set(true); - modal_action("my_modal_5","open"); - }}/> + <Button + text="Create Dimension".to_string() + on_click={ + let edit_clone = edit_signal.to_owned(); + move |_| { + edit_clone.set(false); + set_open_form.set(true); + modal_action("my_modal_5", "open"); + } + } + /> </div> <div> - { - move || { + {move || { let value = dimensions.get(); match value { Some(v) => { @@ -446,12 +450,13 @@ pub fn Dimensions() -> impl IntoView { .iter() .map(|ele| { let mut ele_map = json!(ele).as_object().unwrap().clone(); - ele_map.insert( - "created_at".to_string(), - json!(ele.created_at.format("%v").to_string()) - ); ele_map - }) + .insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()), + ); + ele_map + }) .collect::<Vec<Map<String, Value>>>() .to_owned(); view! { @@ -473,4 +478,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Dimensions/types.rs b/crates/frontend/src/pages/Dimensions/types.rs index b9727c942..64586b5ee 100644 --- a/crates/frontend/src/pages/Dimensions/types.rs +++ b/crates/frontend/src/pages/Dimensions/types.rs @@ -1,7 +1,6 @@ -use chrono::{DateTime, NaiveDateTime, Utc}; -use derive_more::{Deref, DerefMut}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; +use serde_json::Value; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Dimension { @@ -10,4 +9,4 @@ pub struct Dimension { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 623ffbf31..5bf584dde 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -9,7 +9,6 @@ use web_sys::SubmitEvent; use crate::{ api::{fetch_default_config, fetch_dimensions}, components::{ - experiment_form::experiment_form::ExperimentForm, table::{table::Table, types::Column}, }, pages::Home::Home::extract_and_format, @@ -259,7 +258,9 @@ fn experiment_detail_view( </div> <div class="stat w-2/12"> <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value text-sm">{move || experiment.get().traffic_percentage}</div> + <div class="stat-value text-sm"> + {move || experiment.get().traffic_percentage} + </div> </div> <div class="stat w-2/12"> <div class="stat-title">Created by</div> @@ -280,8 +281,7 @@ fn experiment_detail_view( </div> </div> - </div> - <div class="card bg-base-100 max-w-screen shadow m-5"> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> <div class="card-body"> <h2 class="card-title">Context</h2> <div class="flex flex-row flex-wrap gap-2"> @@ -291,11 +291,7 @@ fn experiment_detail_view( let tokens = context.split("&&"); for token in tokens.into_iter() { let mut t = token.trim().split(" "); - let (dimension, _, value) = ( - t.next(), - t.next(), - t.next() - ); + let (dimension, _, value) = (t.next(), t.next(), t.next()); view.push( view! { <div class="stat w-3/12"> @@ -303,14 +299,18 @@ fn experiment_detail_view( {format!("{}", dimension.unwrap())} </div> <div class="stat-value text-base"> - {format!("{}", &value.unwrap()[1..value.unwrap().chars().count() - 1])} + {format!( + "{}", + &value.unwrap()[1..value.unwrap().chars().count() - 1], + )} </div> </div> }, ); } view - }} + }} + </div> </div> </div> <div class="card bg-base-100 max-w-screen shadow m-5"> @@ -382,7 +382,7 @@ fn add_dialogs( }); }; - let dimensions = create_resource( + let _dimensions = create_resource( move || tenant_rs.get(), |tenant| async { match fetch_dimensions(tenant).await { @@ -392,7 +392,7 @@ fn add_dialogs( }, ); - let default_config = create_resource( + let _default_config = create_resource( move || tenant_rs.get(), |tenant| async { match fetch_default_config(tenant).await { @@ -407,7 +407,9 @@ fn add_dialogs( <dialog id="edit_exp_modal" class="modal"> <div class="modal-box"> <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> + <i class="ri-close-line"></i> + </button> </form> <h3 class="font-bold text-lg">Edit Experiment</h3> // <ExperimentForm @@ -426,7 +428,9 @@ fn add_dialogs( <dialog id="conclude_exp_modal" class="modal"> <div class="modal-box"> <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> + <i class="ri-close-line"></i> + </button> </form> <h3 class="font-bold text-lg">Conclude This Experiment</h3> @@ -488,7 +492,9 @@ fn add_dialogs( <dialog id="ramp_exp_modal" class="modal"> <div class="modal-box"> <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"><i class="ri-close-line"></i></button> + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> + <i class="ri-close-line"></i> + </button> </form> <h3 class="font-bold text-lg">Ramp up with release</h3> <p class="py-4">Increase the traffic being redirected to the variants</p> @@ -508,7 +514,9 @@ fn add_dialogs( } /> - <button class="btn btn-block text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">Set</button> + <button class="btn btn-block text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"> + Set + </button> </form> </div> </dialog> diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 3f0f8b13c..3c889a8aa 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::components::{ experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, - stat::stat::Stat, table::table::Table, Button::Button::Button, + stat::stat::Stat, table::table::Table, button::button::Button, }; use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; @@ -125,11 +125,16 @@ pub fn ExperimentList() -> impl IntoView { let dialog_ele = element .dyn_ref::<web_sys::HtmlDialogElement>(); match dialog_ele { - Some(ele) => { let _ = ele.show_modal(); }, - None => { log!("no modal element"); } + Some(ele) => { + let _ = ele.show_modal(); + } + None => { + log!("no modal element"); + } } } } + text="Create Experiment".to_string() /> </div> @@ -145,16 +150,19 @@ pub fn ExperimentList() -> impl IntoView { .data .iter() .map(|ele| { - let mut ele_map = json!(ele).as_object().unwrap().to_owned(); + let mut ele_map = json!(ele) + .as_object() + .unwrap() + .to_owned(); ele_map .insert( "created_at".to_string(), - json!(ele.created_at.format("%v").to_string()) + json!(ele.created_at.format("%v").to_string()), ); ele_map .insert( "last_modified".to_string(), - json!(ele.last_modified.format("%v").to_string()) + json!(ele.last_modified.format("%v").to_string()), ); ele_map }) @@ -282,7 +290,6 @@ pub fn ExperimentList() -> impl IntoView { dimensions=dim.clone() default_config=def_conf.clone() handle_submit=handle_submit_experiment_form - edit_mode=false /> </div> </div> @@ -294,4 +301,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index cb6d029ad..caf07a3c2 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::{DateTime, Utc}; use derive_more::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -81,4 +81,4 @@ pub struct Variant { pub context_id: Option<String>, pub override_id: Option<String>, pub overrides: Map<String, Value>, -} +} \ No newline at end of file diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 0e7756ed0..a006d5502 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -6,7 +6,7 @@ use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; use crate::{ api::fetch_dimensions, - components::{context_form::context_form::ContextForm, Button::Button::Button}, + components::{context_form::context_form::ContextForm, button::button::Button}, }; #[derive(Deserialize, Serialize, Clone)] @@ -187,7 +187,7 @@ pub fn home() -> impl IntoView { }, ); - let (display_configs_rs, display_configs_ws) = create_signal(true); + let (_display_configs_rs, _display_configs_ws) = create_signal(true); let unstrike = |search_field_prefix: &String, config: &Map<String, Value>| { for (dimension, value) in config.into_iter() { @@ -418,12 +418,14 @@ pub fn home() -> impl IntoView { view! { <tr> <td>{config}</td> - <td>{match value { - Value::String(s) => s, - Value::Number(num) => num.to_string(), - Value::Bool(b) => b.to_string(), - _ => "".into() - }}</td> + <td> + {match value { + Value::String(s) => s, + Value::Number(num) => num.to_string(), + Value::Bool(b) => b.to_string(), + _ => "".into(), + }} + </td> </tr> } @@ -478,12 +480,13 @@ pub fn home() -> impl IntoView { .push( view! { < tr > < td > < span name = format!("{unique_name}-1") class - = "config-name" class : text-black = { ! striked } class : font-bold = { !striked } - class : text - gray - 300 = { striked } > { key } </ span - > </ td > < td > < span name = format!("{unique_name}-2") - class = "config-value" class : text-black = { ! - striked } class : font-bold = { !striked } class : text - gray - 300 = { striked } > { - value } </ span > </ td > </ tr > + = "config-name" class : text - black = { ! striked } class : + font - bold = { ! striked } class : text - gray - 300 = { + striked } > { key } </ span > </ td > < td > < span name = + format!("{unique_name}-2") class = "config-value" class : + text - black = { ! striked } class : font - bold = { ! + striked } class : text - gray - 300 = { striked } > { value + } </ span > </ td > </ tr > }, ) } @@ -493,7 +496,9 @@ pub fn home() -> impl IntoView { .contexts .iter() .map(|context| { - let condition = parse_conditions(extract_and_format(&context.condition)); + let condition = parse_conditions( + extract_and_format(&context.condition), + ); let rows: Vec<_> = context .override_with_keys .iter() @@ -555,12 +560,12 @@ pub fn home() -> impl IntoView { view! { <div class="mb-4 w-8/12 overflow-y-auto max-h-screen"> // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> // </div> {new_context_views} <div class="card bg-base-100 shadow m-6"> @@ -584,6 +589,15 @@ pub fn home() -> impl IntoView { Some(Err(error)) => { vec![ view! { + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> + <div class="error"> {"Failed to fetch config data: "} {error} </div> @@ -591,7 +605,20 @@ pub fn home() -> impl IntoView { ] } None => { - vec![view! { <div class="error">{"No config data fetched"}</div> }] + vec![ + view! { + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> + + <div class="error">{"No config data fetched"}</div> + }, + ] } } })} @@ -599,4 +626,4 @@ pub fn home() -> impl IntoView { </Suspense> </div> } -} +} \ No newline at end of file From 0ee0618440e5aed4374d040bbe34adf7059c7137 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 21 Dec 2023 16:41:38 +0530 Subject: [PATCH 222/352] refactor: fixed warnings, added redirection for home page and script for setting up the project --- .env.example | 2 +- .../src/api/context/handlers.rs | 1 + crates/context-aware-config/src/main.rs | 3 +- crates/frontend/src/api.rs | 54 +++++++++---------- crates/frontend/src/app.rs | 2 +- .../frontend/src/components/Button/Button.rs | 4 +- crates/frontend/src/components/Button/mod.rs | 2 +- .../components/context_form/context_form.rs | 4 +- .../src/components/context_form/utils.rs | 2 +- .../experiment_form/experiment_form.rs | 2 +- .../src/components/experiment_form/types.rs | 2 +- .../src/components/experiment_form/utils.rs | 2 +- crates/frontend/src/components/mod.rs | 2 +- .../src/components/side_nav/side_nav.rs | 5 +- crates/frontend/src/hoc/layout/layout.rs | 3 +- .../pages/ContextOverride/ContextOverride.rs | 53 ++---------------- .../src/pages/DefaultConfig/DefaultConfig.rs | 5 +- .../src/pages/Dimensions/Dimensions.rs | 3 +- crates/frontend/src/pages/Dimensions/types.rs | 2 +- crates/frontend/src/pages/Experiment/mod.rs | 5 +- .../pages/ExperimentList/ExperimentList.rs | 6 +-- .../src/pages/ExperimentList/types.rs | 2 +- crates/frontend/src/pages/Home/Home.rs | 39 ++++++++++++-- crates/frontend/src/pages/Home/mod.rs | 1 + .../frontend/src/pages/NotFound/NotFound.rs | 16 +++--- crates/frontend/src/pages/mod.rs | 1 + crates/frontend/src/utils.rs | 3 +- makefile | 1 + setup.sh | 3 ++ 29 files changed, 113 insertions(+), 117 deletions(-) create mode 100644 setup.sh diff --git a/.env.example b/.env.example index efc2f4a0b..a8a075658 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,6 @@ DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false TENANTS=mjos,sdk_config -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 1407e1353..8f3d1eabe 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,4 +1,5 @@ use crate::helpers::{json_to_sorted_string, validate_context_jsonschema}; +use std::collections::HashMap; use crate::{ api::{ context::types::{ diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 9eba6936e..cafa825d8 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -6,7 +6,7 @@ mod logger; mod middlewares; use crate::middlewares::audit_response_header::{AuditHeader, TableName}; -use actix_web::{web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; +use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use api::*; use dashboard_auth::{ middleware::DashboardAuth, @@ -199,6 +199,7 @@ async fn main() -> Result<()> { // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) // serve the favicon from /favicon.ico + .service(web::redirect("/", "/admin")) .service(favicon) .leptos_routes( leptos_options.to_owned(), diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index a2847ac0c..3406b9bde 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,5 +1,3 @@ -use leptos::*; - use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; // #[derive(Debug, Serialize, Deserialize, Clone)] @@ -46,30 +44,30 @@ pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, } } -pub fn dimension_resource( - tenant: ReadSignal<String>, -) -> Resource<String, Vec<Dimension>> { - create_blocking_resource( - move || tenant.get(), - |tenant| async { - match fetch_dimensions(tenant).await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ) -} +//pub fn dimension_resource( +// tenant: ReadSignal<String>, +//) -> Resource<String, Vec<Dimension>> { +// create_blocking_resource( +// move || tenant.get(), +// |tenant| async { +// match fetch_dimensions(tenant).await { +// Ok(data) => data, +// Err(_) => vec![], +// } +// }, +// ) +//} -pub fn default_config_resource( - tenant: ReadSignal<String>, -) -> Resource<String, Vec<DefaultConfig>> { - create_blocking_resource( - move || tenant.get(), - |tenant| async { - match fetch_default_config(tenant).await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ) -} +//pub fn default_config_resource( +// tenant: ReadSignal<String>, +//) -> Resource<String, Vec<DefaultConfig>> { +// create_blocking_resource( +// move || tenant.get(), +// |tenant| async { +// match fetch_default_config(tenant).await { +// Ok(data) => data, +// Err(_) => vec![], +// } +// }, +//) +//} diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index f5379cb95..d972b304b 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -44,7 +44,7 @@ pub fn App() -> impl IntoView { path="admin/:tenant/experiments/:id" view=ExperimentPage /> - <Route ssr=SsrMode::PartiallyBlocked path="" view=Home/> + <Route ssr=SsrMode::PartiallyBlocked path="/admin" view=Home/> <Route ssr=SsrMode::PartiallyBlocked path="/admin/:tenant/default-config" diff --git a/crates/frontend/src/components/Button/Button.rs b/crates/frontend/src/components/Button/Button.rs index b1ac6d75c..bec5c76bc 100644 --- a/crates/frontend/src/components/Button/Button.rs +++ b/crates/frontend/src/components/Button/Button.rs @@ -2,7 +2,7 @@ use leptos::*; use web_sys::MouseEvent; #[component] -pub fn Button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl IntoView { +pub fn button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl IntoView { view! { <button class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" @@ -12,4 +12,4 @@ pub fn Button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl In <i class="ri-edit-2-line ml-2"></i> </button> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/Button/mod.rs b/crates/frontend/src/components/Button/mod.rs index 8ddf4525c..aa200ca23 100644 --- a/crates/frontend/src/components/Button/mod.rs +++ b/crates/frontend/src/components/Button/mod.rs @@ -1 +1 @@ -pub mod button; \ No newline at end of file +pub mod button; diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index ce0f427fa..6c445eaa7 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -14,6 +14,8 @@ pub fn ContextForm<NF>( where NF: Fn(Vec<(String, String, String)>) + 'static, { + let _has_dimensions = dimensions.len() > 0; + let (context, set_context) = create_signal(context.clone()); let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); @@ -213,4 +215,4 @@ where </Show> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 52def6f55..c843e3727 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -72,4 +72,4 @@ pub async fn create_context( StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), _ => Err("Internal Server Error".to_string()), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index e859ea8c7..d68d91477 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -265,4 +265,4 @@ where </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs index 2aeb05610..9bb2ee0f9 100644 --- a/crates/frontend/src/components/experiment_form/types.rs +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -8,4 +8,4 @@ pub struct ExperimentCreateRequest { pub context: Value, pub variants: Vec<Variant>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index eafc905ab..d11f07a4e 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -33,4 +33,4 @@ pub async fn create_experiment( StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), _ => Err("Internal Server Error".to_string()), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 889511971..a01a9d667 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -7,4 +7,4 @@ pub mod override_form; pub mod pagination; pub mod side_nav; pub mod stat; -pub mod table; \ No newline at end of file +pub mod table; diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 07f58e33a..4d5e732e5 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -57,7 +57,10 @@ pub fn SideNav() -> impl IntoView { view! { <div class="max-w-xs z-990 fixed my-4 ml-4 block w-full h-full flex-wrap inset-y-0 items-center justify-between overflow-y-auto rounded-2xl border-0 bg-white p-0 shadow-none -translate-x-full transition-transform duration-200 xl:left-0 xl:translate-x-0 xl:bg-transparent"> <div class="h-19.5"> - <A href="/" class="block px-8 py-6 m-0 text-sm whitespace-nowrap text-slate-700"> + <A + href="/admin" + class="block px-8 py-6 m-0 text-sm whitespace-nowrap text-slate-700" + > <span class="ml-1 font-semibold transition-all duration-200"> Superposition Platform </span> diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 857d25dca..45a6ad885 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -4,6 +4,7 @@ use leptos_router::*; #[component] pub fn Layout(children: Children) -> impl IntoView { + let _params = use_params_map(); let location = use_location(); let tenant = match location @@ -29,4 +30,4 @@ pub fn Layout(children: Children) -> impl IntoView { </main> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index f2f47f9be..f6855dd78 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,13 +1,12 @@ use std::rc::Rc; use crate::api::{fetch_default_config, fetch_dimensions}; +use crate::components::button::button::Button; +use crate::components::condition_pills::condition_pills::ConditionPills; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; -use crate::components::button::button::Button; use crate::pages::DefaultConfig::types::Config; -// use leptos::spawn_local; -use crate::components::condition_pills::condition_pills::ConditionPills; use crate::utils::modal_action; use leptos::*; use reqwest::StatusCode; @@ -357,52 +356,6 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { } } -fn parse_conditions(input: String) -> Vec<(String, String, String)> { - let mut conditions = Vec::new(); - let operators = vec!["==", "in"]; - - // Split the string by "and" and iterate over each condition - for condition in input.split("&&") { - let mut parts = Vec::new(); - let mut operator_found = ""; - - // Check for each operator - for operator in &operators { - if condition.contains(operator) { - operator_found = operator; - parts = condition.split(operator).map(|s| s.trim()).collect(); - - // TODO: add this when context update is enabled - if parts.len() == 2 && operator == &"in" { - parts.swap(0, 1); - } - - break; - } - } - - if parts.len() == 2 { - let mut key = parts[0].to_string(); - let mut op = operator_found.to_string(); - let mut val = parts[1].to_string(); - // Add a space after key - key.push(' '); - if op == "==".to_string() { - val = val.trim_matches('"').to_string(); - op = "is".to_string(); - } else { - val = val.trim_matches('"').to_string(); - op = "has".to_string(); - } - op.push(' '); - - conditions.push((key, op, val)); - } - } - - conditions -} - #[component] pub fn ContextOverride() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); @@ -524,4 +477,4 @@ pub fn ContextOverride() -> impl IntoView { </div> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index e5dca3057..34d334775 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -3,8 +3,8 @@ use std::rc::Rc; use crate::components::table::{table::Table, types::Column}; -use crate::components::stat::stat::Stat; use crate::components::button::button::Button; +use crate::components::stat::stat::Stat; use crate::pages::DefaultConfig::types::Config; use crate::pages::ExperimentList::utils::fetch_default_config; use crate::utils::modal_action; @@ -429,6 +429,7 @@ pub fn DefaultConfig() -> impl IntoView { } } /> + </div> <Table table_style="font-mono".to_string() @@ -445,4 +446,4 @@ pub fn DefaultConfig() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 35ef16abe..cdb6eb35d 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -439,6 +439,7 @@ pub fn Dimensions() -> impl IntoView { } } /> + </div> <div> @@ -478,4 +479,4 @@ pub fn Dimensions() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Dimensions/types.rs b/crates/frontend/src/pages/Dimensions/types.rs index 64586b5ee..432fe065b 100644 --- a/crates/frontend/src/pages/Dimensions/types.rs +++ b/crates/frontend/src/pages/Dimensions/types.rs @@ -9,4 +9,4 @@ pub struct Dimension { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 5bf584dde..02406cfdf 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -8,9 +8,7 @@ use web_sys::SubmitEvent; use crate::{ api::{fetch_default_config, fetch_dimensions}, - components::{ - table::{table::Table, types::Column}, - }, + components::table::{table::Table, types::Column}, pages::Home::Home::extract_and_format, }; @@ -303,6 +301,7 @@ fn experiment_detail_view( "{}", &value.unwrap()[1..value.unwrap().chars().count() - 1], )} + </div> </div> }, diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index 3c889a8aa..e68000b83 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -6,8 +6,8 @@ use chrono::{prelude::Utc, TimeZone}; use serde::{Deserialize, Serialize}; use crate::components::{ - experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, - stat::stat::Stat, table::table::Table, button::button::Button, + button::button::Button, experiment_form::experiment_form::ExperimentForm, + pagination::pagination::Pagination, stat::stat::Stat, table::table::Table, }; use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; @@ -301,4 +301,4 @@ pub fn ExperimentList() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs index caf07a3c2..fc0b6ce4c 100644 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ b/crates/frontend/src/pages/ExperimentList/types.rs @@ -81,4 +81,4 @@ pub struct Variant { pub context_id: Option<String>, pub override_id: Option<String>, pub overrides: Map<String, Value>, -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index a006d5502..dcbeb9c51 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -6,7 +6,7 @@ use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; use crate::{ api::fetch_dimensions, - components::{context_form::context_form::ContextForm, button::button::Button}, + components::{button::button::Button, context_form::context_form::ContextForm}, }; #[derive(Deserialize, Serialize, Clone)] @@ -199,9 +199,10 @@ pub fn home() -> impl IntoView { let search_field_prefix = gen_name_id( search_field_prefix, dimension, - &value.as_str().unwrap_or( - &value.to_string().trim_matches('"')[..] - ).to_string(), + &value + .as_str() + .unwrap_or(&value.to_string().trim_matches('"')[..]) + .to_string(), ); logging::log!("search field prefix {:#?}", search_field_prefix); let config_name_elements = document() @@ -425,6 +426,7 @@ pub fn home() -> impl IntoView { Value::Bool(b) => b.to_string(), _ => "".into(), }} + </td> </tr> @@ -598,6 +600,15 @@ pub fn home() -> impl IntoView { // </label> // </div> + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> + <div class="error"> {"Failed to fetch config data: "} {error} </div> @@ -616,6 +627,24 @@ pub fn home() -> impl IntoView { // </label> // </div> + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> + + // <div class="form-control p-10"> + // <label class="cursor-pointer label"> + // <span class="label-text ml-1">Display All Configs</span> + // <input type="checkbox" class="toggle bg-purple-600" + // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) + // checked=move || display_configs_rs.get() /> + // </label> + // </div> + <div class="error">{"No config data fetched"}</div> }, ] @@ -626,4 +655,4 @@ pub fn home() -> impl IntoView { </Suspense> </div> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/pages/Home/mod.rs b/crates/frontend/src/pages/Home/mod.rs index 1629028ea..22b9ee779 100644 --- a/crates/frontend/src/pages/Home/mod.rs +++ b/crates/frontend/src/pages/Home/mod.rs @@ -1 +1,2 @@ +#![allow(non_snake_case)] pub mod Home; diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs index f65456641..af3170add 100644 --- a/crates/frontend/src/pages/NotFound/NotFound.rs +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -1,20 +1,20 @@ use leptos::*; #[component] -pub fn NotFound() -> impl IntoView { +pub fn not_found() -> impl IntoView { // set an HTTP status code 404 // this is feature gated because it can only be done during // initial server-side rendering // if you navigate to the 404 page subsequently, the status // code will not be set because there is not a new HTTP request // to the server - #[cfg(feature = "ssr")] - { - // this can be done inline because it's synchronous - // if it were async, we'd use a server function - let resp = expect_context::<leptos_actix::ResponseOptions>(); - resp.set_status(actix_web::http::StatusCode::NOT_FOUND); - } + //#[cfg(feature = "ssr")] + //{ + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + // let resp = expect_context::<leptos_actix::ResponseOptions>(); + //resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + //} view! { <h1>"Not Found"</h1> } } diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 7b0d5ae80..fe3ccec7f 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] pub mod ContextOverride; pub mod DefaultConfig; pub mod Dimensions; diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index a64ef7778..d6244e056 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,3 +1,4 @@ +#[warn(unused_must_use)] use leptos::*; use wasm_bindgen::JsCast; @@ -10,7 +11,7 @@ pub fn modal_action(name: &str, action: &str) { if action == "close" { el.close(); } else { - el.show_modal(); + let _ = el.show_modal(); } } } else { diff --git a/makefile b/makefile index 688f31942..7fc5ca37a 100644 --- a/makefile +++ b/makefile @@ -102,6 +102,7 @@ run: # make setup cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + cd crates/frontend && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: diff --git a/setup.sh b/setup.sh new file mode 100644 index 000000000..3885846e2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,3 @@ +#!bin/sh +make setup && make tenant TENANT=mjos && make tenant TENANT=sdk_config +npm i \ No newline at end of file From 1d710f3c2c6b92c3a32ce28c72a3de4486b33d64 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 21 Dec 2023 16:41:38 +0530 Subject: [PATCH 223/352] refactor: fixed warnings, added redirection for home page and script for setting up the project --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index f3a6ff618..5cd1c3a37 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,7 @@ leptosfmt wasm-pack leptosfmt + curl ( rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; targets = [ "wasm32-unknown-unknown" ]; From 6e9b3293144266c119ef8c1465ccc56656f15f1d Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Fri, 22 Dec 2023 14:44:26 +0530 Subject: [PATCH 224/352] fix: minor docs update --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 361e1fde9..1fdb885ea 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ * run these commands ```bash $ nix develop - $ make run + $ sh setup.sh + $ make run OR + $ make run -e DOCKER_DNS=localhost ``` * If you get something like this in the output, you are good to go ```bash From 00c36a695549e8fb64e1140f46abc32986e8a029 Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Fri, 22 Dec 2023 18:18:59 +0530 Subject: [PATCH 225/352] fix: cleanup code --- .../condition_pills/condition_pills.rs | 2 +- .../src/components/condition_pills/utils.rs | 92 ++++----- .../pages/ContextOverride/ContextOverride.rs | 89 +------- .../src/pages/DefaultConfig/DefaultConfig.rs | 26 --- .../src/pages/DefaultConfig/helper.rs | 67 ------ .../frontend/src/pages/DefaultConfig/mod.rs | 4 +- crates/frontend/src/pages/Experiment/mod.rs | 3 +- .../src/pages/ExperimentList/utils.rs | 10 +- crates/frontend/src/pages/Home/Home.rs | 193 +----------------- crates/frontend/src/pages/mod.rs | 18 ++ .../src/pages/{DefaultConfig => }/types.rs | 14 +- 11 files changed, 86 insertions(+), 432 deletions(-) delete mode 100644 crates/frontend/src/pages/DefaultConfig/helper.rs rename crates/frontend/src/pages/{DefaultConfig => }/types.rs (100%) diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs index 3603e1cee..a2ce47012 100644 --- a/crates/frontend/src/components/condition_pills/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -3,7 +3,7 @@ use leptos::*; use serde_json::Value; #[component] -pub fn ConditionPills(context: Value) -> impl IntoView { +pub fn ContextPills(context: Value) -> impl IntoView { let condition = extract_and_format(&context); let ctx_values = parse_conditions(condition.clone()); diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs index 9f721eb7d..f4ca37da5 100644 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -1,5 +1,51 @@ use serde_json::Value; +pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { + let mut conditions = Vec::new(); + let operators = vec!["==", "in"]; + + // Split the string by "&&" and iterate over each condition + for condition in input.split("&&") { + let mut parts = Vec::new(); + let mut operator_found = ""; + + // Check for each operator + for operator in &operators { + if condition.contains(operator) { + operator_found = operator; + parts = condition.split(operator).map(|s| s.trim()).collect(); + + // TODO: add this when context update is enabled + if parts.len() == 2 && operator == &"in" { + parts.swap(0, 1); + } + + break; + } + } + + if parts.len() == 2 { + let mut key = parts[0].to_string(); + let mut op = operator_found.to_string(); + let mut val = parts[1].to_string(); + // Add a space after key + key.push(' '); + if op == "==".to_string() { + val = val.trim_matches('"').to_string(); + op = "is".to_string(); + } else { + val = val.trim_matches('"').to_string(); + op = "has".to_string(); + } + op.push(' '); + + conditions.push((key, op, val)); + } + } + + conditions +} + pub fn extract_and_format(condition: &Value) -> String { if condition.is_object() && condition.get("and").is_some() { // Handling complex "and" conditions @@ -65,49 +111,3 @@ fn format_condition(condition: &Value) -> String { "Invalid Condition".to_string() } - -pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { - let mut conditions = Vec::new(); - let operators = vec!["==", "in"]; - - // Split the string by "and" and iterate over each condition - for condition in input.split("&&") { - let mut parts = Vec::new(); - let mut operator_found = ""; - - // Check for each operator - for operator in &operators { - if condition.contains(operator) { - operator_found = operator; - parts = condition.split(operator).map(|s| s.trim()).collect(); - - // TODO: add this when context update is enabled - if parts.len() == 2 && operator == &"in" { - parts.swap(0, 1); - } - - break; - } - } - - if parts.len() == 2 { - let mut key = parts[0].to_string(); - let mut op = operator_found.to_string(); - let mut val = parts[1].to_string(); - // Add a space after key - key.push(' '); - if op == "==".to_string() { - val = val.trim_matches('"').to_string(); - op = "is".to_string(); - } else { - val = val.trim_matches('"').to_string(); - op = "has".to_string(); - } - op.push(' '); - - conditions.push((key, op, val)); - } - } - - conditions -} diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index f6855dd78..f36eeca39 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -2,30 +2,17 @@ use std::rc::Rc; use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::button::button::Button; -use crate::components::condition_pills::condition_pills::ConditionPills; +use crate::components::condition_pills::condition_pills::ContextPills; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; -use crate::pages::DefaultConfig::types::Config; +use crate::pages::fetch_config; use crate::utils::modal_action; use leptos::*; use reqwest::StatusCode; use serde_json::{json, Map, Value}; use web_sys::MouseEvent; -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} - #[component] fn ContextModalForm<NF>(handle_change: NF) -> impl IntoView where @@ -206,71 +193,7 @@ where } } -pub fn extract_and_format(condition: &Value) -> String { - if condition.is_object() && condition.get("and").is_some() { - // Handling complex "and" conditions - let empty_vec = vec![]; - let conditions_json = condition - .get("and") - .and_then(|val| val.as_array()) - .unwrap_or(&empty_vec); // Default to an empty vector if not an array - - let mut formatted_conditions = Vec::new(); - for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); - } - formatted_conditions.join(" && ") - } else { - // Handling single conditions - format_condition(condition) - } -} - -fn format_condition(condition: &Value) -> String { - if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { - let empty_vec = vec![]; - let operands = condition[operator].as_array().unwrap_or(&empty_vec); - - // Handling the "in" operator differently - if operator.as_str() == "in" { - let left_operand = &operands[0]; - let right_operand = &operands[1]; - - let left_str = if left_operand.is_string() { - format!("\"{}\"", left_operand.as_str().unwrap()) - } else { - format!("{}", left_operand) - }; - - if right_operand.is_object() && right_operand["var"].is_string() { - let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); - } - } - - // Handling regular operators - if let Some(first_operand) = operands.get(0) { - if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); - if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } - } - } - } - } - - "Invalid Condition".to_string() -} #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { @@ -428,17 +351,17 @@ pub fn ContextOverride() -> impl IntoView { context_views .push( view! { - <div class="rounded-lg shadow-md bg-white dark:bg-gray-800 p-6 shadow-md"> + <div class="rounded-lg shadow bg-base-100 p-6 shadow"> <div class="flex justify-between"> <div class="flex items-center space-x-4"> - <h3 class="card-title text-base timeline-box text-gray-800 dark:text-white bg-white shadow-md font-mono"> + <h3 class="card-title text-base timeline-box text-gray-800 bg-base-100 shadow-md font-mono"> "Condition" </h3> <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> - <ConditionPills context=context.condition.clone()/> + <ContextPills context=context.condition.clone()/> </div> - <button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> + <button class="p-2 rounded hover:bg-gray-200 transition-colors"> <i class="ri-edit-line text-blue-500"></i> </button> </div> diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 34d334775..b6769373b 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -5,7 +5,6 @@ use crate::components::table::{table::Table, types::Column}; use crate::components::button::button::Button; use crate::components::stat::stat::Stat; -use crate::pages::DefaultConfig::types::Config; use crate::pages::ExperimentList::utils::fetch_default_config; use crate::utils::modal_action; use js_sys; @@ -21,31 +20,6 @@ pub struct RowData { pub value: String, } -// fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { -// // Parse the input string into a serde_json::Value -// let parsed = serde_json::from_str::<Value>(input) -// .expect("Failed to parse JSON"); - -// // Ensure the Value is an Array and convert it to Vec<Value> -// match parsed { -// Value::Array(arr) => arr, -// _ => panic!("Input is not a JSON array"), -// } -// } - -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} - pub async fn create_default_config( tenant: String, key: String, diff --git a/crates/frontend/src/pages/DefaultConfig/helper.rs b/crates/frontend/src/pages/DefaultConfig/helper.rs deleted file mode 100644 index f0cfafa78..000000000 --- a/crates/frontend/src/pages/DefaultConfig/helper.rs +++ /dev/null @@ -1,67 +0,0 @@ -use serde_json::Value; - -pub fn extract_and_format(condition: &Value) -> String { - if condition.is_object() && condition.get("and").is_some() { - // Handling complex "and" conditions - let empty_vec = vec![]; - let conditions_json = condition - .get("and") - .and_then(|val| val.as_array()) - .unwrap_or(&empty_vec); // Default to an empty vector if not an array - - let mut formatted_conditions = Vec::new(); - for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); - } - - formatted_conditions.join(" and ") - } else { - // Handling single conditions - format_condition(condition) - } -} - -fn format_condition(condition: &Value) -> String { - if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { - let empty_vec = vec![]; - let operands = condition[operator].as_array().unwrap_or(&empty_vec); - - // Handling the "in" operator differently - if operator.as_str() == "in" { - let left_operand = &operands[0]; - let right_operand = &operands[1]; - - let left_str = if left_operand.is_string() { - format!("\"{}\"", left_operand.as_str().unwrap()) - } else { - format!("{}", left_operand) - }; - - if right_operand.is_object() && right_operand["var"].is_string() { - let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); - } - } - - // Handling regular operators - if let Some(first_operand) = operands.get(0) { - if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); - if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } - } - } - } - } - - "Invalid Condition".to_string() -} diff --git a/crates/frontend/src/pages/DefaultConfig/mod.rs b/crates/frontend/src/pages/DefaultConfig/mod.rs index 53d091161..e54b2b181 100644 --- a/crates/frontend/src/pages/DefaultConfig/mod.rs +++ b/crates/frontend/src/pages/DefaultConfig/mod.rs @@ -1,3 +1 @@ -pub mod DefaultConfig; -pub mod helper; -pub mod types; +pub mod DefaultConfig; \ No newline at end of file diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 02406cfdf..57e19cbe7 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -8,8 +8,7 @@ use web_sys::SubmitEvent; use crate::{ api::{fetch_default_config, fetch_dimensions}, - components::table::{table::Table, types::Column}, - pages::Home::Home::extract_and_format, + components::{table::{table::Table, types::Column}, condition_pills::utils::extract_and_format}, }; #[derive( diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 08721f694..8d74e1006 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,6 +1,6 @@ use super::types::{DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; use crate::components::{ - condition_pills::condition_pills::ConditionPills, table::types::Column, + condition_pills::condition_pills::ContextPills, table::types::Column, }; use core::time::Duration; use leptos::*; @@ -161,9 +161,9 @@ pub fn experiment_table_columns() -> Vec<Column> { None, Some(|value: &str, _| { let badge_color = match value { - "CREATED" => "badge-success", + "CREATED" => "badge-info", "INPROGRESS" => "badge-warning", - "CONCLUDED" => "badge-info", + "CONCLUDED" => "badge-success", &_ => "info", }; let class = format!("badge {}", badge_color); @@ -188,7 +188,7 @@ pub fn experiment_table_columns() -> Vec<Column> { view! { <div class="inline-flex flex-col gap-y-2"> - <ConditionPills context=context /> + <ContextPills context=context /> </div> } .into_view() @@ -199,7 +199,7 @@ pub fn experiment_table_columns() -> Vec<Column> { None, Some(|value: &str, _| { let label = match value { - "null" => "".to_string(), + "null" => "¯\\_(ツ)_/¯".to_string(), other => other.to_string(), }; diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index dcbeb9c51..57e9eb1b3 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -1,39 +1,13 @@ use leptos::*; -use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use wasm_bindgen::JsCast; use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; use crate::{ api::fetch_dimensions, - components::{button::button::Button, context_form::context_form::ContextForm}, + components::{button::button::Button, context_form::context_form::ContextForm, condition_pills::utils::{parse_conditions, extract_and_format}}, pages::fetch_config, }; -#[derive(Deserialize, Serialize, Clone)] -pub struct Config { - pub contexts: Vec<Context>, - pub overrides: Map<String, Value>, - pub default_configs: Map<String, Value>, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Context { - pub id: String, - pub condition: Value, - pub override_with_keys: [String; 1], -} - -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let url = "http://localhost:8080/config"; - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); @@ -53,118 +27,6 @@ async fn resolve_config(tenant: String, context: String) -> Result<Value, String } } -fn parse_conditions(input: String) -> Vec<(String, String, String)> { - let mut conditions = Vec::new(); - let operators = vec!["==", "in"]; - - // Split the string by "&&" and iterate over each condition - for condition in input.split("&&") { - let mut parts = Vec::new(); - let mut operator_found = ""; - - // Check for each operator - for operator in &operators { - if condition.contains(operator) { - operator_found = operator; - parts = condition.split(operator).map(|s| s.trim()).collect(); - - // TODO: add this when context update is enabled - if parts.len() == 2 && operator == &"in" { - parts.swap(0, 1); - } - - break; - } - } - - if parts.len() == 2 { - let mut key = parts[0].to_string(); - let mut op = operator_found.to_string(); - let mut val = parts[1].to_string(); - // Add a space after key - key.push(' '); - if op == "==".to_string() { - val = val.trim_matches('"').to_string(); - op = "is".to_string(); - } else { - val = val.trim_matches('"').to_string(); - op = "has".to_string(); - } - op.push(' '); - - conditions.push((key, op, val)); - } - } - - conditions -} - -pub fn extract_and_format(condition: &Value) -> String { - if condition.is_object() && condition.get("and").is_some() { - // Handling complex "and" conditions - let empty_vec = vec![]; - let conditions_json = condition - .get("and") - .and_then(|val| val.as_array()) - .unwrap_or(&empty_vec); // Default to an empty vector if not an array - - let mut formatted_conditions = Vec::new(); - for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); - } - - formatted_conditions.join(" && ") - } else { - // Handling single conditions - format_condition(condition) - } -} - -fn format_condition(condition: &Value) -> String { - if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { - let empty_vec = vec![]; - let operands = condition[operator].as_array().unwrap_or(&empty_vec); - - // Handling the "in" operator differently - if operator.as_str() == "in" { - let left_operand = &operands[0]; - let right_operand = &operands[1]; - - let left_str = if left_operand.is_string() { - format!("\"{}\"", left_operand.as_str().unwrap()) - } else { - format!("{}", left_operand) - }; - - if right_operand.is_object() && right_operand["var"].is_string() { - let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); - } - } - - // Handling regular operators - if let Some(first_operand) = operands.get(0) { - if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); - if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } - } - } - } - } - - "Invalid Condition".to_string() -} - fn gen_name_id(s0: &String, s1: &String, s2: &String) -> String { format!("{s0}::{s1}::{s2}") } @@ -561,14 +423,6 @@ pub fn home() -> impl IntoView { vec![ view! { <div class="mb-4 w-8/12 overflow-y-auto max-h-screen"> - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> {new_context_views} <div class="card bg-base-100 shadow m-6"> <div class="card-body"> @@ -591,24 +445,6 @@ pub fn home() -> impl IntoView { Some(Err(error)) => { vec![ view! { - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> - - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> - <div class="error"> {"Failed to fetch config data: "} {error} </div> @@ -618,33 +454,6 @@ pub fn home() -> impl IntoView { None => { vec![ view! { - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> - - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> - - // <div class="form-control p-10"> - // <label class="cursor-pointer label"> - // <span class="label-text ml-1">Display All Configs</span> - // <input type="checkbox" class="toggle bg-purple-600" - // on:click=move |_| display_configs_ws.update(|toggle| *toggle = !*toggle) - // checked=move || display_configs_rs.get() /> - // </label> - // </div> - <div class="error">{"No config data fetched"}</div> }, ] diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index fe3ccec7f..9830dbe73 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,4 +1,7 @@ #![allow(non_snake_case)] + +use self::types::Config; + pub mod ContextOverride; pub mod DefaultConfig; pub mod Dimensions; @@ -6,3 +9,18 @@ pub mod Experiment; pub mod ExperimentList; pub mod Home; pub mod NotFound; +pub mod types; + +// Utils segments found here + +pub async fn fetch_config(tenant: String) -> Result<Config, String> { + let client = reqwest::Client::new(); + let url = "http://localhost:8080/config"; + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} \ No newline at end of file diff --git a/crates/frontend/src/pages/DefaultConfig/types.rs b/crates/frontend/src/pages/types.rs similarity index 100% rename from crates/frontend/src/pages/DefaultConfig/types.rs rename to crates/frontend/src/pages/types.rs index 5f04ae551..adca618bf 100644 --- a/crates/frontend/src/pages/DefaultConfig/types.rs +++ b/crates/frontend/src/pages/types.rs @@ -1,16 +1,16 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -#[derive(Deserialize, Serialize, Clone)] -pub struct Config { - pub contexts: Vec<Context>, - pub overrides: Map<String, Value>, - pub default_configs: Map<String, Value>, -} - #[derive(Deserialize, Serialize, Clone)] pub struct Context { pub id: String, pub condition: Value, pub override_with_keys: [String; 1], } + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} From cad8e7fe4d6f1cbcb8c6196c8bee123ee0aa7ca3 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Tue, 2 Jan 2024 11:34:00 +0530 Subject: [PATCH 226/352] fix: fixed ci-test to support multi-tenant setup --- .env.example | 2 +- crates/context-aware-config/src/main.rs | 7 +- .../experimentation-platform/src/db/schema.rs | 36 --- .../{Button/Button.rs => button/button.rs} | 0 .../src/components/{Button => button}/mod.rs | 0 .../dimension_form/dimension_form.rs | 248 ++++++++++++++++++ .../src/components/dimension_form/mod.rs | 3 + .../src/components/dimension_form/types.rs | 9 + .../src/components/dimension_form/utils.rs | 42 +++ .../pages/ContextOverride/ContextOverride.rs | 2 - .../frontend/src/pages/DefaultConfig/mod.rs | 2 +- crates/frontend/src/pages/Experiment/mod.rs | 5 +- crates/frontend/src/pages/Home/Home.rs | 14 +- crates/frontend/src/pages/mod.rs | 2 +- makefile | 52 ++-- .../cac/Context/Create Context/event.test.js | 7 +- .../cac/Context/Create Context/request.json | 7 +- .../cac/Context/Delete Context/event.test.js | 3 +- .../cac/Context/Delete Context/request.json | 7 +- postman/cac/Context/Get Context/request.json | 7 +- postman/cac/Context/List Context/request.json | 7 +- .../cac/Context/Move Context/event.test.js | 3 +- postman/cac/Context/Move Context/request.json | 7 +- .../cac/Context/Update Context/event.test.js | 7 +- .../cac/Context/Update Context/request.json | 7 +- .../Add default-config key/event.test.js | 3 +- .../Add default-config key/request.json | 7 +- .../Dimension/Create Dimension/request.json | 7 +- postman/cac/audit log/get_logs/request.json | 10 +- postman/cac/config/Get Config/request.json | 7 +- postman/cac/config/resolve/request.json | 10 +- .../Conclude/event.test.js | 16 +- .../Conclude/request.json | 4 +- .../Create Experiment 2/event.prerequest.js | 4 +- .../Create Experiment 2/event.test.js | 14 +- .../Create Experiment 2/request.json | 4 +- .../Create Experiment/event.prerequest.js | 4 +- .../Create Experiment/event.test.js | 14 +- .../Create Experiment/request.json | 4 +- .../Get Experiment/request.json | 10 +- .../request.json | 7 +- .../request.json | 7 +- .../request.json | 7 +- .../Ramp/event.test.js | 5 +- .../Ramp/request.json | 7 +- .../Update Override Keys/event.prerequest.js | 2 +- .../Update Override Keys/event.test.js | 14 +- .../Update Override Keys/request.json | 2 +- scripts/create-tenant.sh | 9 +- 49 files changed, 516 insertions(+), 148 deletions(-) rename crates/frontend/src/components/{Button/Button.rs => button/button.rs} (100%) rename crates/frontend/src/components/{Button => button}/mod.rs (100%) create mode 100644 crates/frontend/src/components/dimension_form/dimension_form.rs create mode 100644 crates/frontend/src/components/dimension_form/mod.rs create mode 100644 crates/frontend/src/components/dimension_form/types.rs create mode 100644 crates/frontend/src/components/dimension_form/utils.rs diff --git a/.env.example b/.env.example index a8a075658..9bbaec95c 100644 --- a/.env.example +++ b/.env.example @@ -22,7 +22,7 @@ MAX_DB_CONNECTION_POOL_SIZE=3 DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false -TENANTS=mjos,sdk_config +TENANTS=dev,test TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index cafa825d8..99e4342e7 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -96,6 +96,11 @@ async fn main() -> Result<()> { ) .await; + let ui_redirect_path = match tenants.iter().next() { + Some(tenant) => format!("/admin/{}/resolve", tenant), + None => String::from("/admin"), + }; + /****** EXPERIMENTATION PLATFORM ENVs *********/ let allow_same_keys_overlapping_ctx: bool = @@ -199,7 +204,7 @@ async fn main() -> Result<()> { // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) // serve the favicon from /favicon.ico - .service(web::redirect("/", "/admin")) + .service(web::redirect("/", ui_redirect_path.to_string())) .service(favicon) .leptos_routes( leptos_options.to_owned(), diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 796f436a7..143ddfa86 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -6,39 +6,6 @@ pub mod sql_types { pub struct ExperimentStatusType; } -diesel::table! { - contexts (id) { - id -> Varchar, - value -> Json, - override_id -> Varchar, - created_at -> Timestamptz, - created_by -> Varchar, - priority -> Int4, - #[sql_name = "override"] - override_ -> Json, - } -} - -diesel::table! { - default_configs (key) { - key -> Varchar, - value -> Json, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } -} - -diesel::table! { - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - created_at -> Timestamptz, - created_by -> Varchar, - schema -> Json, - } -} - diesel::table! { event_log (id, timestamp) { id -> Uuid, @@ -229,9 +196,6 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - contexts, - default_configs, - dimensions, event_log, event_log_y2023m08, event_log_y2023m09, diff --git a/crates/frontend/src/components/Button/Button.rs b/crates/frontend/src/components/button/button.rs similarity index 100% rename from crates/frontend/src/components/Button/Button.rs rename to crates/frontend/src/components/button/button.rs diff --git a/crates/frontend/src/components/Button/mod.rs b/crates/frontend/src/components/button/mod.rs similarity index 100% rename from crates/frontend/src/components/Button/mod.rs rename to crates/frontend/src/components/button/mod.rs diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs new file mode 100644 index 000000000..44340494d --- /dev/null +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -0,0 +1,248 @@ +use super::types::DimensionCreateReq; +use super::utils::{create_dimension, parse_string_to_json_value_vec}; +use crate::components::button::button::Button; +use leptos::*; +use serde_json::json; +use web_sys::MouseEvent; + +#[component] +pub fn dimension_form<NF>( + edit: bool, + priority: u16, + dimension_name: String, + dimension_type: String, + dimension_pattern: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let (priority, set_priority) = create_signal(priority); + let (dimension_name, set_dimension_name) = create_signal(dimension_name); + let (dimension_type, set_dimension_type) = create_signal(dimension_type); + let (dimension_pattern, set_dimension_pattern) = create_signal(dimension_pattern); + + let (show_labels, set_show_labels) = create_signal(false); + + let (error_message, set_error_message) = create_signal("".to_string()); + + let on_submit = move |ev: MouseEvent| { + ev.prevent_default(); + let f_priority = priority.get(); + let f_name = dimension_name.get(); + let f_type = dimension_type.get(); + let f_pattern = dimension_pattern.get(); + + let f_schema = match f_type.as_str() { + "number" => { + json!({ + "type": f_type.to_string() + }) + } + "Enum" => { + json!({ + "type": "string", + "enum": parse_string_to_json_value_vec(f_pattern.as_str()) + }) + } + "Pattern" => { + json!({ + "type": "string", + "pattern": f_pattern.to_string() + }) + } + _ => { + json!(f_pattern.to_string()) + } + }; + + let payload = DimensionCreateReq { + dimension: f_name, + priority: f_priority, + schema: f_schema, + }; + + let handle_submit_clone = handle_submit.clone(); + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_dimension(tenant_rs.get(), payload.clone()).await; + + match result { + Ok(_) => { + handle_submit(); + // modal_action("my_modal_5","close"); + } + Err(e) => { + set_error_message.set(e); + // Handle error + // Consider logging or displaying the error + } + } + } + }); + }; + view! { + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Dimension</span> + </label> + <input + disabled=edit + type="text" + placeholder="Dimension" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=dimension_name.get() + on:change=move |ev| { + let value = event_target_value(&ev); + set_dimension_name.set(value); + } + /> + + </div> + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_dimension_type.set("number".to_string()); + } + "Enum" => { + set_dimension_type.set("Enum".to_string()); + set_dimension_pattern + .set(format!("{:?}", vec!["android", "web", "ios"])); + } + "Pattern" => { + set_dimension_type.set("Pattern".to_string()); + set_dimension_pattern.set(".*".to_string()); + } + _ => { + set_dimension_type.set("Other".to_string()); + set_dimension_pattern.set("".to_string()); + } + }; + } + + class="select select-bordered" + > + <option disabled selected> + Set Schema + </option> + + <option + value="number" + selected=move || { dimension_type.get() == "number".to_string() } + > + "Number" + </option> + <option + value="Enum" + selected=move || { dimension_type.get() == "Enum".to_string() } + > + "String (Enum)" + </option> + <option + value="Pattern" + selected=move || { dimension_type.get() == "Pattern".to_string() } + > + "String (regex)" + </option> + <option + value="Other" + selected=move || { dimension_type.get() == "Other".to_string() } + > + "Other" + </option> + </select> + + {move || { + view! { + <Show when=move || (dimension_type.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority.get() + on:change=move |ev| { + logging::log!( + "{:?}", event_target_value(& ev).parse::< u16 > () + ); + match event_target_value(&ev).parse::<u16>() { + Ok(i_prio) => set_priority.set(i_prio), + Err(e) => logging::log!("{e}"), + }; + } + /> + + </div> + </Show> + + <Show when=move || (show_labels.get() && (dimension_type.get() != "number"))> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=priority.get() + on:change=move |ev| { + logging::log!( + "{:?}", event_target_value(& ev).parse::< u16 > () + ); + match event_target_value(&ev).parse::<u16>() { + Ok(i_prio) => set_priority.set(i_prio), + Err(e) => logging::log!("{e}"), + }; + } + /> + + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {dimension_type.get()} + </span> + </label> + <textarea + type="text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_dimension_pattern.set(value); + } + > + + {dimension_pattern.get()} + </textarea> + + </div> + </Show> + } + }} + + <div class="form-control mt-6"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + { + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } + + </form> + } +} \ No newline at end of file diff --git a/crates/frontend/src/components/dimension_form/mod.rs b/crates/frontend/src/components/dimension_form/mod.rs new file mode 100644 index 000000000..f6e5d65d2 --- /dev/null +++ b/crates/frontend/src/components/dimension_form/mod.rs @@ -0,0 +1,3 @@ +pub mod dimension_form; +pub mod utils; +pub mod types; \ No newline at end of file diff --git a/crates/frontend/src/components/dimension_form/types.rs b/crates/frontend/src/components/dimension_form/types.rs new file mode 100644 index 000000000..7b87b6ccd --- /dev/null +++ b/crates/frontend/src/components/dimension_form/types.rs @@ -0,0 +1,9 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize, Clone)] +pub struct DimensionCreateReq { + pub dimension: String, + pub priority: u16, + pub schema: Value, +} \ No newline at end of file diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs new file mode 100644 index 000000000..40424740d --- /dev/null +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -0,0 +1,42 @@ +use super::types::DimensionCreateReq; +use leptos::logging; +use reqwest::StatusCode; +use serde_json::Value; + +pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { + // Parse the input string into a serde_json::Value + let parsed = serde_json::from_str::<Value>(input); + + // Ensure the Value is an Array and convert it to Vec<Value> + match parsed { + Ok(Value::Array(arr)) => arr, + _ => { + logging::log!("Not a valid json in the input"); + vec![] + } + } +} + +pub async fn create_dimension( + tenant: String, + payload: DimensionCreateReq, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{host}/dimension"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} \ No newline at end of file diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index f36eeca39..320c90090 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -193,8 +193,6 @@ where } } - - #[component] fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let (context_condition, set_context_condition) = diff --git a/crates/frontend/src/pages/DefaultConfig/mod.rs b/crates/frontend/src/pages/DefaultConfig/mod.rs index e54b2b181..1a618fd7e 100644 --- a/crates/frontend/src/pages/DefaultConfig/mod.rs +++ b/crates/frontend/src/pages/DefaultConfig/mod.rs @@ -1 +1 @@ -pub mod DefaultConfig; \ No newline at end of file +pub mod DefaultConfig; diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 57e19cbe7..79caf87f6 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -8,7 +8,10 @@ use web_sys::SubmitEvent; use crate::{ api::{fetch_default_config, fetch_dimensions}, - components::{table::{table::Table, types::Column}, condition_pills::utils::extract_and_format}, + components::{ + condition_pills::utils::extract_and_format, + table::{table::Table, types::Column}, + }, }; #[derive( diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 57e9eb1b3..f2ecc5cd3 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -5,10 +5,14 @@ use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; use crate::{ api::fetch_dimensions, - components::{button::button::Button, context_form::context_form::ContextForm, condition_pills::utils::{parse_conditions, extract_and_format}}, pages::fetch_config, + components::{ + button::button::Button, + condition_pills::utils::{extract_and_format, parse_conditions}, + context_form::context_form::ContextForm, + }, + pages::fetch_config, }; - async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); let url = format!("http://localhost:8080/config/resolve?{context}"); @@ -452,11 +456,7 @@ pub fn home() -> impl IntoView { ] } None => { - vec![ - view! { - <div class="error">{"No config data fetched"}</div> - }, - ] + vec![view! { <div class="error">{"No config data fetched"}</div> }] } } })} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 9830dbe73..27bbc7101 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -23,4 +23,4 @@ pub async fn fetch_config(tenant: String) -> Result<Config, String> { } Err(e) => Err(e.to_string()), } -} \ No newline at end of file +} diff --git a/makefile b/makefile index 7fc5ca37a..cf1a4afa9 100644 --- a/makefile +++ b/makefile @@ -16,13 +16,15 @@ SHELL := /usr/bin/env bash validate-psql-connection cac +cleanup: + -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) + -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") + db-init: diesel migration run --locked-schema --config-file=crates/context-aware-config/diesel.toml -diesel migration run --locked-schema --config-file=crates/experimentation-platform/diesel.toml -cac-migration: - -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) - -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") +cac-migration: cleanup docker-compose up -d postgres cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -33,9 +35,7 @@ cac-migration: diesel migration run --config-file=crates/context-aware-config/diesel.toml docker-compose down -exp-migration: - -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) - -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") +exp-migration: cleanup docker-compose up -d postgres cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -46,9 +46,7 @@ exp-migration: diesel migration run --config-file=crates/experimentation-platform/diesel.toml docker-compose down -migration: - make cac-migration - make exp-migration +migration: cac-migration exp-migration legacy_db_setup: grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/legacy-db-setup.sh @@ -62,7 +60,8 @@ validate-aws-connection: validate-psql-connection: pg_isready -h $(DOCKER_DNS) -p 5432 -ci-setup: + +env-setup: docker-compose up -d postgres localstack cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -70,19 +69,28 @@ ci-setup: do echo "waiting for postgres, localstack bootup"; \ sleep 0.5; \ done + +test-tenant: + make tenant TENANT='test' + +dev-tenant: + make tenant TENANT='dev' + +ci-setup: env-setup test-tenant + npm ci --loglevel=error + make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & + while ! grep -q "starting in Actix" test_logs; \ + do echo "ci-test: waiting for bootup..." && sleep 4; \ + done # NOTE: `make db-init` finally starts a postgres container and runs all the migrations with locked-schema option # to prevent update of schema.rs for both cac and experimentation. # NOTE: The container spinned-up here is the actual container being used in development - make db-init - make legacy_db_setup echo setup completed successfully!!! -setup: +setup: migration env-setup test-tenant dev-tenant # NOTE: `make migration` is being used to run the migrations for cac and experimentation in isolation, # otherwise the tables and types of cac and experimentation spill into each others schema.rs # NOTE: The container spinned up are stopped and removed after the work is done. - make migration - make ci-setup kill: -pkill -f target/debug/context-aware-config & @@ -91,30 +99,20 @@ cac: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ cargo run --color always --bin context-aware-config --no-default-features --features=ssr -run: - -make kill +run: kill cd crates/frontend && wasm-pack build --target=web --debug --no-default-features --features=hydrate cargo build --color always while ! make validate-psql-connection validate-aws-connection; \ do echo "waiting for postgres, localstack bootup"; \ sleep 0.5; \ done - # make setup cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env cd crates/frontend && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css make cac -e DOCKER_DNS=$(DOCKER_DNS) -ci-test: - -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) - -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") - npm ci --loglevel=error +ci-test: cleanup ci-setup cargo test - make ci-setup - make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & - while ! grep -q "starting in Actix" test_logs; \ - do echo "ci-test: waiting for bootup..." && sleep 4; \ - done npm run test ci-build: diff --git a/postman/cac/Context/Create Context/event.test.js b/postman/cac/Context/Create Context/event.test.js index ff5ca44b9..1bd87dec4 100644 --- a/postman/cac/Context/Create Context/event.test.js +++ b/postman/cac/Context/Create Context/event.test.js @@ -6,6 +6,7 @@ function getConfigAndTest(context_id, override_id, expected_condition, expected_ method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -35,9 +36,9 @@ function getConfigAndTest(context_id, override_id, expected_condition, expected_ const context_override_ids = context.override_with_keys; pm.expect(context_override_ids).to.include(override_id); - + console.log(`Checking override=${override_id} in overrides object`); - const override = overrides[override_id]; + const override = overrides[override_id]; console.log(`Expected => ${JSON.stringify(expected_override)}`); console.log(`Actual => ${JSON.stringify(override)}`); pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); @@ -74,4 +75,4 @@ pm.test("Check if context is added", function () { getConfigAndTest(context_id, override_id, condition, override); -}); +}); \ No newline at end of file diff --git a/postman/cac/Context/Create Context/request.json b/postman/cac/Context/Create Context/request.json index 2dd61b9ce..6bf6048f0 100644 --- a/postman/cac/Context/Create Context/request.json +++ b/postman/cac/Context/Create Context/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -42,4 +47,4 @@ "context" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Context/Delete Context/event.test.js b/postman/cac/Context/Delete Context/event.test.js index 4482b0ed4..88a601ebc 100644 --- a/postman/cac/Context/Delete Context/event.test.js +++ b/postman/cac/Context/Delete Context/event.test.js @@ -11,6 +11,7 @@ pm.test("Fetch for context should fail with 404", function () { method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -23,4 +24,4 @@ pm.test("Fetch for context should fail with 404", function () { pm.expect(response.code).to.be.eq(404); }); -}) +}) \ No newline at end of file diff --git a/postman/cac/Context/Delete Context/request.json b/postman/cac/Context/Delete Context/request.json index c550b9968..ed19fe4c4 100644 --- a/postman/cac/Context/Delete Context/request.json +++ b/postman/cac/Context/Delete Context/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -17,4 +22,4 @@ "{{context_id}}" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Context/Get Context/request.json b/postman/cac/Context/Get Context/request.json index 62678bad8..948e54cd4 100644 --- a/postman/cac/Context/Get Context/request.json +++ b/postman/cac/Context/Get Context/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -17,4 +22,4 @@ "{{context_id}}" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Context/List Context/request.json b/postman/cac/Context/List Context/request.json index ecdd44a16..78c1e260f 100644 --- a/postman/cac/Context/List Context/request.json +++ b/postman/cac/Context/List Context/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -17,4 +22,4 @@ "list" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Context/Move Context/event.test.js b/postman/cac/Context/Move Context/event.test.js index 4d0bbc826..3f35669d1 100644 --- a/postman/cac/Context/Move Context/event.test.js +++ b/postman/cac/Context/Move Context/event.test.js @@ -6,6 +6,7 @@ function getConfigAndTest(context_id, override_id, expected_condition, expected_ method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -79,4 +80,4 @@ pm.test("Check if context is added", function () { getConfigAndTest(context_id, override_id, condition, override); -}); +}); \ No newline at end of file diff --git a/postman/cac/Context/Move Context/request.json b/postman/cac/Context/Move Context/request.json index a6eabcb79..c19dadefb 100644 --- a/postman/cac/Context/Move Context/request.json +++ b/postman/cac/Context/Move Context/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -44,4 +49,4 @@ "{{context_id}}" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Context/Update Context/event.test.js b/postman/cac/Context/Update Context/event.test.js index ff5ca44b9..1bd87dec4 100644 --- a/postman/cac/Context/Update Context/event.test.js +++ b/postman/cac/Context/Update Context/event.test.js @@ -6,6 +6,7 @@ function getConfigAndTest(context_id, override_id, expected_condition, expected_ method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -35,9 +36,9 @@ function getConfigAndTest(context_id, override_id, expected_condition, expected_ const context_override_ids = context.override_with_keys; pm.expect(context_override_ids).to.include(override_id); - + console.log(`Checking override=${override_id} in overrides object`); - const override = overrides[override_id]; + const override = overrides[override_id]; console.log(`Expected => ${JSON.stringify(expected_override)}`); console.log(`Actual => ${JSON.stringify(override)}`); pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); @@ -74,4 +75,4 @@ pm.test("Check if context is added", function () { getConfigAndTest(context_id, override_id, condition, override); -}); +}); \ No newline at end of file diff --git a/postman/cac/Context/Update Context/request.json b/postman/cac/Context/Update Context/request.json index 7e3588e57..8d1043f78 100644 --- a/postman/cac/Context/Update Context/request.json +++ b/postman/cac/Context/Update Context/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -42,4 +47,4 @@ "context" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Default Config/Add default-config key/event.test.js b/postman/cac/Default Config/Add default-config key/event.test.js index 8ec09bb5c..28b30c2c3 100644 --- a/postman/cac/Default Config/Add default-config key/event.test.js +++ b/postman/cac/Default Config/Add default-config key/event.test.js @@ -6,6 +6,7 @@ function getConfigAndTest(key, value) { method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -30,4 +31,4 @@ pm.test("201 check", function () { pm.test("Check if key added to default config", function () { const key = "key1", value = "value1"; getConfigAndTest(key, value); -}); +}); \ No newline at end of file diff --git a/postman/cac/Default Config/Add default-config key/request.json b/postman/cac/Default Config/Add default-config key/request.json index de3a62cd9..c5345661f 100644 --- a/postman/cac/Default Config/Add default-config key/request.json +++ b/postman/cac/Default Config/Add default-config key/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -37,4 +42,4 @@ "key1" ] } -} +} \ No newline at end of file diff --git a/postman/cac/Dimension/Create Dimension/request.json b/postman/cac/Dimension/Create Dimension/request.json index 7f2a9c941..b9b4696da 100644 --- a/postman/cac/Dimension/Create Dimension/request.json +++ b/postman/cac/Dimension/Create Dimension/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -37,4 +42,4 @@ "dimension" ] } -} +} \ No newline at end of file diff --git a/postman/cac/audit log/get_logs/request.json b/postman/cac/audit log/get_logs/request.json index c9ef2f644..3052131db 100644 --- a/postman/cac/audit log/get_logs/request.json +++ b/postman/cac/audit log/get_logs/request.json @@ -1,6 +1,12 @@ { "method": "GET", - "header": [], + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], "url": { "raw": "{{host}}/audit", "host": [ @@ -10,4 +16,4 @@ "audit" ] } -} +} \ No newline at end of file diff --git a/postman/cac/config/Get Config/request.json b/postman/cac/config/Get Config/request.json index 21f267f7e..bc973fd23 100644 --- a/postman/cac/config/Get Config/request.json +++ b/postman/cac/config/Get Config/request.json @@ -5,6 +5,11 @@ "key": "Authorization", "value": "Bearer {{token}}", "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -16,4 +21,4 @@ "config" ] } -} +} \ No newline at end of file diff --git a/postman/cac/config/resolve/request.json b/postman/cac/config/resolve/request.json index c962d39c8..fc4ab4343 100644 --- a/postman/cac/config/resolve/request.json +++ b/postman/cac/config/resolve/request.json @@ -1,6 +1,12 @@ { "method": "GET", - "header": [], + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], "url": { "raw": "{{host}}/config/resolve?clientId=zee5", "host": [ @@ -17,4 +23,4 @@ } ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Conclude/event.test.js b/postman/experimentation-platform/Conclude/event.test.js index 54793d344..a59c78306 100644 --- a/postman/experimentation-platform/Conclude/event.test.js +++ b/postman/experimentation-platform/Conclude/event.test.js @@ -9,7 +9,8 @@ function fetch_config_n_test(variants, winner_variant_id) { 'url': `${host}/config`, 'header': { 'Authorization': `Bearer ${token}`, - 'Contet-Type': 'application/json' + 'Contet-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -25,7 +26,7 @@ function fetch_config_n_test(variants, winner_variant_id) { const winner_variant = variants.find(variant => variant.id === winner_variant_id); const winner_variant_override_id = winner_variant.override_id; - + // there should be only one context with the winner variant override id const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id)); console.log("Context with winner variant override"); @@ -35,10 +36,10 @@ function fetch_config_n_test(variants, winner_variant_id) { // there should be 0 contexts with variant as a dimension const contexts_with_variant_dim = contexts .filter( - (context) => + (context) => context.condition.and ?.map( - (condition) => + (condition) => Object.keys(condition) .map((k) => condition[k][0].var === "variant") .reduce((p, c) => p || c, false)) @@ -47,7 +48,7 @@ function fetch_config_n_test(variants, winner_variant_id) { pm.expect(contexts_with_variant_dim.length).to.be.eq(0); // checking if winner override exists and is same as the expected override - const winner_variant_context = contexts_with_winner_variant_override[0]; + const winner_variant_context = contexts_with_winner_variant_override[0]; pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1); pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id)); @@ -67,7 +68,8 @@ function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_stat 'url': `${host}/experiments/${experiment_id}`, "header": { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -76,7 +78,7 @@ function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_stat console.log("Failed to fetch experiment"); throw error; } - + const experiment = response.json(); const status = experiment.status; diff --git a/postman/experimentation-platform/Conclude/request.json b/postman/experimentation-platform/Conclude/request.json index 2bf875d5a..af51941aa 100644 --- a/postman/experimentation-platform/Conclude/request.json +++ b/postman/experimentation-platform/Conclude/request.json @@ -13,7 +13,7 @@ }, { "key": "x-tenant", - "value": "mjos", + "value": "test", "type": "default" } ], @@ -39,4 +39,4 @@ "conclude" ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js index 43715a7d8..7433c1cea 100644 --- a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js +++ b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js @@ -13,7 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, - 'x-tenant': 'mjos', + 'x-tenant': 'test', 'Content-Type': 'application/json' }, "body": { @@ -52,7 +52,7 @@ function create_dimensions() { 'url': `${host}/dimension`, 'header': { 'Authorization': `Bearer ${token}`, - 'x-tenant': 'mjos', + 'x-tenant': 'test', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Create Experiment 2/event.test.js b/postman/experimentation-platform/Create Experiment 2/event.test.js index e8337b7e7..65d98a4aa 100644 --- a/postman/experimentation-platform/Create Experiment 2/event.test.js +++ b/postman/experimentation-platform/Create Experiment 2/event.test.js @@ -8,10 +8,11 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; - + pm.sendRequest(getRequest, (error, response) => { if(error) { console.log("Failed to fetch context"); @@ -21,7 +22,7 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid const context = response.json(); /*********** checking contexts created in CAC **********/; - + const variant_override_id = context.override_id; const varaint_context = context.value; @@ -50,7 +51,8 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai 'url': `${host}/experiments/${experiment_id}`, "header": { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -59,7 +61,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai console.log("Failed to fetch experiment"); throw error; } - + const experiment = response.json(); const context = experiment.context; @@ -90,7 +92,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai const expected_override_id = variant.override_id; const expected_override = variant.overrides; const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; - + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); } }); @@ -100,7 +102,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai pm.test("200 OK", function () { const response = pm.response.json(); const experiment_id = response.experiment_id; - + pm.environment.set("experiment_id", experiment_id); pm.response.to.have.status(200); }); diff --git a/postman/experimentation-platform/Create Experiment 2/request.json b/postman/experimentation-platform/Create Experiment 2/request.json index f78771300..d231f32da 100644 --- a/postman/experimentation-platform/Create Experiment 2/request.json +++ b/postman/experimentation-platform/Create Experiment 2/request.json @@ -8,7 +8,7 @@ }, { "key": "x-tenant", - "value": "mjos", + "value": "test", "type": "default" } ], @@ -75,4 +75,4 @@ "experiments" ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/event.prerequest.js b/postman/experimentation-platform/Create Experiment/event.prerequest.js index 516679d96..da93399e2 100644 --- a/postman/experimentation-platform/Create Experiment/event.prerequest.js +++ b/postman/experimentation-platform/Create Experiment/event.prerequest.js @@ -13,7 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, - 'x-tenant': 'mjos', + 'x-tenant': 'test', 'Content-Type': 'application/json' }, "body": { @@ -52,7 +52,7 @@ function create_dimensions() { 'url': `${host}/dimension`, 'header': { 'Authorization': `Bearer ${token}`, - 'x-tenant': 'mjos', + 'x-tenant': 'test', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Create Experiment/event.test.js b/postman/experimentation-platform/Create Experiment/event.test.js index 8fd9f2b6a..2acd7253e 100644 --- a/postman/experimentation-platform/Create Experiment/event.test.js +++ b/postman/experimentation-platform/Create Experiment/event.test.js @@ -8,10 +8,11 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; - + pm.sendRequest(getRequest, (error, response) => { if(error) { console.log("Failed to fetch context"); @@ -21,7 +22,7 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid const context = response.json(); /*********** checking contexts created in CAC **********/; - + const variant_override_id = context.override_id; const varaint_context = context.value; @@ -50,7 +51,8 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai 'url': `${host}/experiments/${experiment_id}`, "header": { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -59,7 +61,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai console.log("Failed to fetch experiment"); throw error; } - + const experiment = response.json(); const context = experiment.context; @@ -90,7 +92,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai const expected_override_id = variant.override_id; const expected_override = variant.overrides; const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; - + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); } }); @@ -100,7 +102,7 @@ function fetch_experiment_n_test(experiment_id, expected_context, expected_varai pm.test("200 OK", function () { const response = pm.response.json(); const experiment_id = response.experiment_id; - + pm.environment.set("experiment_id", experiment_id); pm.response.to.have.status(200); }); diff --git a/postman/experimentation-platform/Create Experiment/request.json b/postman/experimentation-platform/Create Experiment/request.json index 519ccb6aa..030e4c00f 100644 --- a/postman/experimentation-platform/Create Experiment/request.json +++ b/postman/experimentation-platform/Create Experiment/request.json @@ -8,7 +8,7 @@ }, { "key": "x-tenant", - "value": "mjos", + "value": "test", "type": "default" }, { @@ -80,4 +80,4 @@ "experiments" ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Get Experiment/request.json b/postman/experimentation-platform/Get Experiment/request.json index 0b12e225b..ddb344f65 100644 --- a/postman/experimentation-platform/Get Experiment/request.json +++ b/postman/experimentation-platform/Get Experiment/request.json @@ -1,6 +1,12 @@ { "method": "GET", - "header": [], + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], "url": { "raw": "{{host}}/experiments/{{experiment_id}}", "host": [ @@ -11,4 +17,4 @@ "{{experiment_id}}" ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json index 3031385c2..2c2789238 100644 --- a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json @@ -9,6 +9,11 @@ "key": "If-Modified-Since", "value": "Thu, 01 Jan 1970 00:00:00 +0000", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -34,4 +39,4 @@ } ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json index 8823b1df7..e0df22d43 100644 --- a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json @@ -9,6 +9,11 @@ "key": "If-Modified-Since", "value": "Wed, 01 Jan 2070 00:00:00 +0000", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -34,4 +39,4 @@ } ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json index 3a3ebbc86..3e863c985 100644 --- a/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json +++ b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json @@ -4,6 +4,11 @@ { "key": "Authorization", "value": "Bearer {{token}}" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "url": { @@ -37,4 +42,4 @@ } ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Ramp/event.test.js b/postman/experimentation-platform/Ramp/event.test.js index 533ebfc9f..e0ad446c2 100644 --- a/postman/experimentation-platform/Ramp/event.test.js +++ b/postman/experimentation-platform/Ramp/event.test.js @@ -7,7 +7,8 @@ function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) { 'url': `${host}/experiments/${experiment_id}`, "header": { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -16,7 +17,7 @@ function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) { console.log("Failed to fetch experiment"); throw error; } - + const experiment = response.json(); console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`); pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage); diff --git a/postman/experimentation-platform/Ramp/request.json b/postman/experimentation-platform/Ramp/request.json index 0809b22ed..9c4e4edc9 100644 --- a/postman/experimentation-platform/Ramp/request.json +++ b/postman/experimentation-platform/Ramp/request.json @@ -10,6 +10,11 @@ "key": "Content-Type", "value": "application/json", "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" } ], "body": { @@ -34,4 +39,4 @@ "ramp" ] } -} +} \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/event.prerequest.js b/postman/experimentation-platform/Update Override Keys/event.prerequest.js index 17f71ffcf..49475ab42 100644 --- a/postman/experimentation-platform/Update Override Keys/event.prerequest.js +++ b/postman/experimentation-platform/Update Override Keys/event.prerequest.js @@ -13,7 +13,7 @@ function create_default_config_keys() { 'url': `${host}/default-config/${key}`, "header": { 'Authorization': `Bearer ${token}`, - 'x-tenant': 'mjos', + 'x-tenant': 'test', 'Content-Type': 'application/json' }, "body": { diff --git a/postman/experimentation-platform/Update Override Keys/event.test.js b/postman/experimentation-platform/Update Override Keys/event.test.js index c55a11d1d..ddb6a0175 100644 --- a/postman/experimentation-platform/Update Override Keys/event.test.js +++ b/postman/experimentation-platform/Update Override Keys/event.test.js @@ -8,10 +8,11 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid method: 'GET', header: { 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; - + pm.sendRequest(getRequest, (error, response) => { if(error) { console.log("Failed to fetch context"); @@ -21,7 +22,7 @@ function fetch_context_n_test(context_id, expected_override_id, expected_overrid const context = response.json(); /*********** checking contexts created in CAC **********/; - + const variant_override_id = context.override_id; const varaint_context = context.value; @@ -50,7 +51,8 @@ function fetch_experiment_n_test(experiment_id, expected_varaints, expected_vari 'url': `${host}/experiments/${experiment_id}`, "header": { 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'x-tenant': 'test', } }; @@ -59,7 +61,7 @@ function fetch_experiment_n_test(experiment_id, expected_varaints, expected_vari console.log("Failed to fetch experiment"); throw error; } - + const experiment = response.json(); const variants = experiment.variants; @@ -84,7 +86,7 @@ function fetch_experiment_n_test(experiment_id, expected_varaints, expected_vari const expected_override_id = variant.override_id; const expected_override = variant.overrides; const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; - + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); } }); @@ -94,7 +96,7 @@ function fetch_experiment_n_test(experiment_id, expected_varaints, expected_vari pm.test("200 OK", function () { const response = pm.response.json(); const experiment_id = response.experiment_id; - + pm.environment.set("experiment_id", experiment_id); pm.response.to.have.status(200); }); diff --git a/postman/experimentation-platform/Update Override Keys/request.json b/postman/experimentation-platform/Update Override Keys/request.json index ad47235bb..145614cf2 100644 --- a/postman/experimentation-platform/Update Override Keys/request.json +++ b/postman/experimentation-platform/Update Override Keys/request.json @@ -8,7 +8,7 @@ }, { "key": "x-tenant", - "value": "mjos", + "value": "test", "type": "default" } ], diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index 6799796a5..c8e0dbafc 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -9,16 +9,17 @@ echo "DB URL ==> $DB_URL" CAC_SCHEMA="${TENANT}_cac" EXP_SCHEMA="${TENANT}_experimentation" -echo "Creating Schemas ==> $CAC_SCHEMA, $EXP_SCHEMA" - cp -r "crates/context-aware-config/migrations/." "crates/context-aware-config/${TENANT}_migrations" find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${CAC_SCHEMA}/g" cp -r "crates/experimentation-platform/migrations/." "crates/experimentation-platform/${TENANT}_migrations" find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${EXP_SCHEMA}/g" -find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" | xargs psql "$DB_URL" -f -find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" | xargs psql "$DB_URL" -f +echo "Creating $CAC_SCHEMA" +find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; + +echo "Creating $EXP_SCHEMA" +find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; rm -rf "crates/context-aware-config/${TENANT}_migrations" rm -rf "crates/experimentation-platform/${TENANT}_migrations" \ No newline at end of file From 3311c5e1b1d1a94d5b807f0a24a886b752663b9e Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 4 Jan 2024 15:26:27 +0530 Subject: [PATCH 227/352] fix: fixed tenant hydration bug --- .../src/components/side_nav/side_nav.rs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 4d5e732e5..c8f438176 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -2,6 +2,7 @@ use std::{collections::HashSet, env::VarError, str::FromStr}; use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; + use leptos::{logging::log, *}; use leptos_router::{use_location, use_navigate, A}; @@ -24,22 +25,16 @@ pub fn SideNav() -> impl IntoView { let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); let (app_routes, set_app_routes) = create_signal(create_routes(&tenant_rs.get())); - let tenants: HashSet<String> = get_from_env_unsafe::<String>("TENANTS") - .unwrap_or("m,s".into()) - .split(",") - .map(|tenant| tenant.to_string()) - .collect::<HashSet<String>>(); - - let mut view_vector = vec![]; - for tenant in tenants.into_iter() { - if tenant == tenant_rs.get() { - view_vector - .push(view! { <option selected=true>{tenant}</option> }.into_view()); - } else { - view_vector.push(view! { <option>{tenant}</option> }.into_view()); - } + async fn fetch_tenants(tenants: &str) -> HashSet<String> { + get_from_env_unsafe::<String>(tenants) + .unwrap_or("m,s".into()) + .split(",") + .map(|tenant| tenant.to_string()) + .collect() } + let tenants = create_blocking_resource(|| (), move |_| fetch_tenants("TENANTS")); + create_effect(move |_| { let current_path = location.pathname.get(); @@ -66,6 +61,9 @@ pub fn SideNav() -> impl IntoView { </span> </A> </div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> <select value=tenant_rs.get() on:change=move |change| { @@ -94,7 +92,19 @@ pub fn SideNav() -> impl IntoView { class="select w-full max-w-xs shadow-md" > - {view_vector} + { + match tenants.get() { + Some(tenants_data) => { + let tenants_clone = tenants_data.clone(); + tenants_clone.iter().map(|tenant| { + view! { + <option selected=tenant == &tenant_rs.get()>{tenant}</option> + } + }).collect::<Vec<_>>() + }, + _ => vec![view! { <option disabled=true>{"Loading tenants..."}</option> }] + } + } </select> // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> @@ -120,6 +130,7 @@ pub fn SideNav() -> impl IntoView { </ul> </div> + </Suspense> </div> } } From ec7aecc60fd3a63226886eabdf2f65ea13a0217a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 4 Jan 2024 16:07:26 +0530 Subject: [PATCH 228/352] chore: formatted code + cleanup --- .../src/api/context/handlers.rs | 3 +- .../src/api/default_config/handlers.rs | 4 +- .../src/components/side_nav/side_nav.rs | 130 +++++++++--------- crates/frontend/src/pages/Experiment/mod.rs | 30 ++-- 4 files changed, 91 insertions(+), 76 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 8f3d1eabe..3cd6f0a8c 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,5 +1,4 @@ use crate::helpers::{json_to_sorted_string, validate_context_jsonschema}; -use std::collections::HashMap; use crate::{ api::{ context::types::{ @@ -554,4 +553,4 @@ async fn bulk_operations( Ok(_) => Ok(web::Json(resp)), Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error } -} \ No newline at end of file +} diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index b907155b8..0b88dcd6f 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -5,7 +5,7 @@ use crate::{ }; use actix_web::{ error::{ErrorBadRequest, ErrorInternalServerError}, - put, get, + get, put, web::{self, Data, Json}, HttpResponse, Scope, }; @@ -132,4 +132,4 @@ async fn get(db_conn: DbConnection) -> app::Result<Json<Vec<DefaultConfig>>> { let result: Vec<DefaultConfig> = default_configs.get_results(&mut conn)?; Ok(Json(result)) -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index c8f438176..0235351db 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -64,72 +64,76 @@ pub fn SideNav() -> impl IntoView { <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - <select - value=tenant_rs.get() - on:change=move |change| { - let new_tenant = event_target_value(&change); - let location = use_location(); - let mut path_tokens = location - .pathname - .get() - .split("/") - .into_iter() - .map(ToString::to_string) - .collect::<Vec<String>>(); - log!("{}{:?}", new_tenant, path_tokens); - path_tokens.remove(0); - path_tokens.remove(0); - path_tokens.remove(0); - log!("{}{:?}", new_tenant, path_tokens); - let nav = use_navigate(); - set_app_routes.set(create_routes(&new_tenant)); - tenant_ws.set(new_tenant.clone()); - nav( - format!("admin/{new_tenant}/{}", path_tokens.join("/")).as_str(), - Default::default(), - ); - } + <select + value=tenant_rs.get() + on:change=move |change| { + let new_tenant = event_target_value(&change); + let location = use_location(); + let mut path_tokens = location + .pathname + .get() + .split("/") + .into_iter() + .map(ToString::to_string) + .collect::<Vec<String>>(); + log!("{}{:?}", new_tenant, path_tokens); + path_tokens.remove(0); + path_tokens.remove(0); + path_tokens.remove(0); + log!("{}{:?}", new_tenant, path_tokens); + let nav = use_navigate(); + set_app_routes.set(create_routes(&new_tenant)); + tenant_ws.set(new_tenant.clone()); + nav( + format!("admin/{new_tenant}/{}", path_tokens.join("/")).as_str(), + Default::default(), + ); + } - class="select w-full max-w-xs shadow-md" - > - { - match tenants.get() { - Some(tenants_data) => { - let tenants_clone = tenants_data.clone(); - tenants_clone.iter().map(|tenant| { - view! { - <option selected=tenant == &tenant_rs.get()>{tenant}</option> - } - }).collect::<Vec<_>>() - }, - _ => vec![view! { <option disabled=true>{"Loading tenants..."}</option> }] - } - } - </select> - // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> - <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> - <ul class="menu"> - <For - each=move || app_routes.get() - key=|route: &AppRoute| route.key.to_string() - children=move |route: AppRoute| { - let path = route.path.to_string(); - let is_active = location.pathname.get().contains(&path); - view! { - <li class="mt-1 w-full"> - <NavItem - href=route.path.to_string() - icon=route.icon.to_string() - text=route.label.to_string() - is_active=is_active - /> - </li> - } + class="select w-full max-w-xs shadow-md" + > + + {match tenants.get() { + Some(tenants_data) => { + let tenants_clone = tenants_data.clone(); + tenants_clone + .iter() + .map(|tenant| { + view! { + <option selected=tenant + == &tenant_rs.get()>{tenant}</option> + } + }) + .collect::<Vec<_>>() } - /> + _ => vec![view! { <option disabled=true>{"Loading tenants..."}</option> }], + }} - </ul> - </div> + </select> + // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> + <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> + <ul class="menu"> + <For + each=move || app_routes.get() + key=|route: &AppRoute| route.key.to_string() + children=move |route: AppRoute| { + let path = route.path.to_string(); + let is_active = location.pathname.get().contains(&path); + view! { + <li class="mt-1 w-full"> + <NavItem + href=route.path.to_string() + icon=route.icon.to_string() + text=route.label.to_string() + is_active=is_active + /> + </li> + } + } + /> + + </ul> + </div> </Suspense> </div> } diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 79caf87f6..12096331e 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -76,11 +76,15 @@ async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, } } -async fn ramp_experiment(exp_id: &String, percent: u8) -> Result<Experiment, String> { +async fn ramp_experiment( + exp_id: &String, + percent: u8, + tenant: &String, +) -> Result<Experiment, String> { let client = reqwest::Client::new(); match client .patch(format!("http://localhost:8080/experiments/{}/ramp", exp_id)) - .header("x-tenant", "mjos") + .header("x-tenant", tenant) .json(&json!({ "traffic_percentage": percent })) .send() .await @@ -142,7 +146,8 @@ pub fn experiment_page() -> impl IntoView { }> {move || match experiment_info.get() { Some(Ok(experiment)) => { - experiment_detail_view(experiment, experiment_info).into_view() + let tenant = tenant_rs.get(); + experiment_detail_view(tenant, experiment, experiment_info).into_view() } Some(Err(err)) => view! { <h1>{err.to_string()}</h1> }.into_view(), None => view! { <h1>No elements</h1> }.into_view(), @@ -153,6 +158,7 @@ pub fn experiment_page() -> impl IntoView { } fn experiment_detail_view( + tenant: String, initial_data: Experiment, exp_resource: Resource<(String, String), Result<Experiment, String>>, ) -> impl IntoView { @@ -188,6 +194,7 @@ fn experiment_detail_view( {move || { experiment .with(|exp| { + let tenant_clone = tenant.clone(); match exp.status { ExperimentStatusType::CREATED => { view! { @@ -201,11 +208,15 @@ fn experiment_detail_view( <button class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" value=&exp.id - on:click=move |button_event| spawn_local(async move { - let value = event_target_value(&button_event); - let _ = ramp_experiment(&value, 1).await; - exp_resource.refetch(); - }) + on:click=move |button_event| { + let tenant_clone = tenant_clone.clone(); + spawn_local(async move { + let tenant_clone = tenant_clone.clone(); + let value = event_target_value(&button_event); + let _ = ramp_experiment(&value, 1, &tenant_clone).await; + exp_resource.refetch(); + }) + } > <i class="ri-guide-line"></i> @@ -378,7 +389,8 @@ fn add_dialogs( .expect("<input> to exist") .value_as_number() as u8; spawn_local(async move { - let _ = ramp_experiment(&experiment().id, value).await; + let tenant = tenant_rs.get(); + let _ = ramp_experiment(&experiment().id, value, &tenant).await; experiment_ws.refetch(); }); }; From 5759a151352a754a247ddb0235c0611a2ae3401c Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 4 Jan 2024 13:49:34 +0000 Subject: [PATCH 229/352] chore(version): v0.17.0 [skip ci] --- CHANGELOG.md | 47 ++++++++++++++++++++ Cargo.lock | 8 ++-- crates/cac_client/CHANGELOG.md | 6 +++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 17 +++++++ crates/context-aware-config/Cargo.toml | 4 +- crates/experimentation-platform/CHANGELOG.md | 9 ++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 8 ++++ crates/service-utils/Cargo.toml | 2 +- 10 files changed, 96 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8c65364..c1ab40fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,53 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.17.0 - 2024-01-04 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.0 +- cac_client bumped to cac_client-v0.5.0 +- context-aware-config bumped to context-aware-config-v0.14.0 +- service-utils bumped to service-utils-v0.10.0 +### Global changes +#### Bug Fixes +- fixed tenant hydration bug - (cf0e633) - Saurav Suman +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- cleanup code - (4820f31) - Kartik Gajendra +- minor docs update - (04ab586) - Kartik Gajendra +- UI fixes for demo - (4927766) - Kartik Gajendra +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +- context parsing - (d46ca42) - Kartik Gajendra +- resolve UI bugs - (98695a8) - Kartik Gajendra +- dimensions page updates - (5220b36) - ankit.mahato +#### Features +- added validation inside default config form , formatted dates , added disable feature of edit - (cacf20f) - Saurav Suman +- resolve page with unified UI - (e84eb41) - Kartik Gajendra +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed experiment suspense block , added generic button - (117bfc8) - Saurav Suman +- experiment create form - (91371c0) - Shubhranshu Sanjeev +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working resolve page - (81c83d4) - Kartik Gajendra +- added state changes in the form - (b64a227) - Saurav Suman +- testing create form - (d0a5aea) - Kartik Gajendra +- working experiments page - (81b17dc) - Kartik Gajendra +- experiment UI - (72e19e6) - Kartik Gajendra +- added default config and override screen - (cd4267e) - Saurav Suman +- added default config page - (95e909d) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- override and context form - (553e3ad) - Shubhranshu Sanjeev +- dimensions - (c5e94fa) - ankit.mahato +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +- formatted frontend code - (70f873f) - Shubhranshu Sanjeev +#### Refactoring +- fixed warnings, added redirection for home page and script for setting up the project - (a9ee3bd) - Saurav Suman +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + ## v0.16.3 - 2023-12-27 ### Package updates - context-aware-config bumped to context-aware-config-v0.13.2 diff --git a/Cargo.lock b/Cargo.lock index 0acacea64..d19bcdbad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,7 +622,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix-web", "chrono", @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.13.2" +version = "0.14.0" dependencies = [ "actix", "actix-cors", @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.8.2" +version = "0.9.0" dependencies = [ "actix", "actix-web", @@ -3390,7 +3390,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.9.0" +version = "0.10.0" dependencies = [ "actix", "actix-web", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 52e59fb4e..66479b441 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.5.0 - 2024-01-04 +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra + +- - - + ## cac_client-v0.4.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 9b53c14f3..b66d0ba7e 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.4.0" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 2aaeb9d41..416199e16 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.14.0 - 2024-01-04 +#### Bug Fixes +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +#### Refactoring +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + ## context-aware-config-v0.13.2 - 2023-12-27 #### Bug Fixes - [PICAF-25568] array validation for in condition - (a45ac4a) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 4bbab3db5..d54ce97a4 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.13.2" +version = "0.14.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -65,4 +65,4 @@ leptos_router = { version = "0.5.2" } actix-files = { version = "0.6" } anyhow = { workspace = true } -dashboard-auth = { workspace = true } \ No newline at end of file +dashboard-auth = { workspace = true } diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index dd5c146b6..4a501677b 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.9.0 - 2024-01-04 +#### Bug Fixes +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra + +- - - + ## experimentation-platform-v0.8.2 - 2023-11-30 #### Bug Fixes - allow ramp 0 - (b8d49aa) - Kartik Gajendra diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index d9927eed7..725328eac 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.8.2" +version = "0.9.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 48b58716b..db48c4c7d 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.10.0 - 2024-01-04 +#### Bug Fixes +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +#### Features +- experiment UI - (24e1b56) - Kartik Gajendra + +- - - + ## service-utils-v0.9.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index e934b74e2..bba2d1ca2 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.9.0" +version = "0.10.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 55ac190ab585987350adb089109d6d499de994ac Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 4 Jan 2024 19:35:38 +0530 Subject: [PATCH 230/352] fix: fixed build failure due to rust-version --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 996e8eeb7..c892a3ffa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM rust:1.67 as builder +FROM rust:1.73 as builder WORKDIR /build COPY . . RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts RUN --mount=type=ssh cargo build --release RUN cargo build --release -FROM debian:bullseye-slim +FROM debian:bookworm-slim WORKDIR /app ARG SOURCE_COMMIT From c937b68028c385ddb8340efb1cce6b66ac69db8a Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Mon, 8 Jan 2024 20:29:31 +0530 Subject: [PATCH 231/352] fix: frontend build process --- .dockerignore | 2 + .env.example | 2 +- Cargo.toml | 16 ++--- Dockerfile | 31 ++++++++- README.md | 2 +- crates/context-aware-config/src/main.rs | 12 ++-- crates/frontend/src/api.rs | 9 ++- crates/frontend/src/app.rs | 54 +++++++++++++-- .../src/components/context_form/utils.rs | 3 +- .../src/components/dimension_form/utils.rs | 3 +- .../src/components/experiment_form/utils.rs | 3 +- .../src/components/side_nav/side_nav.rs | 57 ++++++---------- .../pages/ContextOverride/ContextOverride.rs | 4 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 4 +- .../src/pages/Dimensions/Dimensions.rs | 4 +- .../frontend/src/pages/Dimensions/helper.rs | 10 +-- crates/frontend/src/pages/Experiment/mod.rs | 65 +++++++++++-------- .../src/pages/ExperimentList/utils.rs | 25 ++----- crates/frontend/src/pages/Home/Home.rs | 10 ++- crates/frontend/src/pages/mod.rs | 5 +- crates/frontend/src/types.rs | 30 ++++++++- crates/frontend/src/utils.rs | 18 ++++- flake.lock | 30 ++++----- flake.nix | 1 - makefile | 21 +++++- 25 files changed, 261 insertions(+), 160 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0c4969132 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target +node-modules \ No newline at end of file diff --git a/.env.example b/.env.example index 9bbaec95c..b1e95bc53 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,6 @@ DASHBOARD_AUTH_ENABLED=false ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false TENANTS=dev,test -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2e9064549..0db7ea292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,21 +10,15 @@ members = [ "crates/frontend" ] - [[workspace.metadata.leptos]] -output-name = "frontend" +name = "cac" bin-package = "context-aware-config" +output-name = "frontend" lib-package = "frontend" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "crates/frontend/styles/style.css" assets-dir = "crates/frontend/assets" -style-file = "frontend/style/output.css" -site-root = "crates/frontend" - -[workspace.metadata.leptos.style] -# Main style file. If scss or sass then it will be compiled to css. -# the parent folder will be watched for changes -file = "style/output.css" -# A https://browsersl.ist query -browserquery = "defaults" [workspace.dependencies] dotenv = "0.15.0" diff --git a/Dockerfile b/Dockerfile index c892a3ffa..3f6868d3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,37 @@ FROM rust:1.73 as builder WORKDIR /build +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 18.19.0 + +RUN mkdir -p $NVM_DIR +RUN curl "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh" | bash \ + && . $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default + +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH="${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}" +RUN node --version + COPY . . RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +RUN npm ci --loglevel=info + +# building frontend +RUN --mount=type=ssh cd crates/frontend \ + && wasm-pack build --target=web --no-default-features --features=hydrate + +# copying .wasm, .js and .css files to target/site directory +RUN mkdir -p target/site && mkdir -p target/site/pkg +RUN cd crates/frontend \ + && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css +RUN mv crates/frontend/pkg target/site/ +RUN cp -a crates/frontend/assets/. target/site/ +RUN cp .env.example .env +# building backend RUN --mount=type=ssh cargo build --release -RUN cargo build --release FROM debian:bookworm-slim WORKDIR /app @@ -14,6 +42,7 @@ ARG CONTEXT_AWARE_CONFIG_VERSION RUN apt-get update && apt-get install -y libpq5 ca-certificates COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config COPY --from=builder /build/Cargo.toml /app/Cargo.toml +COPY --from=builder /build/target/site /app/target/site ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT CMD ["/app/context-aware-config"] \ No newline at end of file diff --git a/README.md b/README.md index 1fdb885ea..7af62f847 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ * run these commands ```bash $ nix develop - $ sh setup.sh + $ make setup $ make run OR $ make run -e DOCKER_DNS=localhost ``` diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 99e4342e7..7fc1a3800 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -198,11 +198,12 @@ async fn main() -> Result<()> { AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), ), ) - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) - // serve other assets from the `assets` directory - .service(Files::new("/assets", format!("{site_root}/assets"))) + /***************************** UI Routes ******************************/ + // .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}"))) // serve the favicon from /favicon.ico .service(web::redirect("/", ui_redirect_path.to_string())) .service(favicon) @@ -222,11 +223,6 @@ async fn main() -> Result<()> { .await } -#[actix_web::get("/style.css")] -async fn css() -> impl actix_web::Responder { - actix_files::NamedFile::open_async("./style/output.css").await -} - fn authenticated_routes() -> AuthenticatedRouteList { let mut route_vector: Vec<(&str, AuthenticatedRoute)> = Vec::new(); route_vector.append(&mut auth::contexts::authenticated_routes()); diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 3406b9bde..56e0a359b 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,4 +1,7 @@ -use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; +use crate::{ + pages::ExperimentList::types::{DefaultConfig, Dimension}, + utils::get_host, +}; // #[derive(Debug, Serialize, Deserialize, Clone)] // pub struct Dimension { @@ -20,7 +23,7 @@ use crate::pages::ExperimentList::types::{DefaultConfig, Dimension}; pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/dimension"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { @@ -33,7 +36,7 @@ pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/default-config"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index d972b304b..9b186817c 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -1,3 +1,6 @@ +use std::env::VarError; +use std::str::FromStr; + use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -10,10 +13,53 @@ use crate::pages::{ DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, Home::Home::Home, NotFound::NotFound::NotFound, }; +use crate::types::{AppEnv, Envs}; + +fn get_from_env_unsafe<F>(name: &str) -> Result<F, VarError> +where + F: FromStr, + <F as FromStr>::Err: std::fmt::Debug, +{ + std::env::var(name) + .map(|val| val.parse().unwrap()) + .map_err(|e| { + return e; + }) +} + +async fn load_envs() -> Envs { + let app_env = get_from_env_unsafe::<AppEnv>("APP_ENV").unwrap_or(AppEnv::DEV); + + let tenants = get_from_env_unsafe::<String>("TENANTS") + .unwrap_or("".into()) + .split(",") + .map(|tenant| tenant.to_string()) + .collect::<Vec<String>>(); + + logging::log!("{:?}", tenants); + + let host = match app_env { + AppEnv::PROD => { + "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" + } + AppEnv::SANDBOX => "https://context-aware.internal.staging.mum.juspay.net", + AppEnv::DEV => "http://localhost:8080", + }; + + Envs { + host: host.to_string(), + app_env, + tenants, + } +} + #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); + + let envs_resource = create_blocking_resource(|| (), |_| load_envs()); + provide_context(envs_resource); view! { <html data-theme="light"> <Stylesheet id="leptos" href="/pkg/style.css"/> @@ -36,15 +82,14 @@ pub fn App() -> impl IntoView { /> <Route ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/experiments" + path="/admin/:tenant/experiments" view=ExperimentList /> <Route ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/experiments/:id" + path="/admin/:tenant/experiments/:id" view=ExperimentPage /> - <Route ssr=SsrMode::PartiallyBlocked path="/admin" view=Home/> <Route ssr=SsrMode::PartiallyBlocked path="/admin/:tenant/default-config" @@ -57,11 +102,10 @@ pub fn App() -> impl IntoView { /> <Route ssr=SsrMode::PartiallyBlocked - path="admin/:tenant/resolve" + path="/admin/:tenant/resolve" view=Home /> <Route path="/*any" view=NotFound/> - </Routes> </Layout> </body> diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index c843e3727..08d429408 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,3 +1,4 @@ +use crate::utils::get_host; use reqwest::StatusCode; use serde_json::{json, Map, Value}; @@ -56,7 +57,7 @@ pub async fn create_context( conditions: Vec<(String, String, String)>, ) -> Result<String, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/context"); let request_payload = construct_request_payload(overrides, conditions); let response = client diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs index 40424740d..652a0c2e7 100644 --- a/crates/frontend/src/components/dimension_form/utils.rs +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -2,6 +2,7 @@ use super::types::DimensionCreateReq; use leptos::logging; use reqwest::StatusCode; use serde_json::Value; +use crate::utils::get_host; pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { // Parse the input string into a serde_json::Value @@ -22,7 +23,7 @@ pub async fn create_dimension( payload: DimensionCreateReq, ) -> Result<String, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/dimension"); let response = client diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index d11f07a4e..a89f0106d 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -1,6 +1,7 @@ use super::types::ExperimentCreateRequest; use crate::components::context_form::utils::construct_context; use crate::pages::ExperimentList::types::Variant; +use crate::utils::get_host; use reqwest::StatusCode; use serde_json::json; @@ -17,7 +18,7 @@ pub async fn create_experiment( }; let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/experiments"); let request_payload = json!(payload); let response = client diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 0235351db..e59acb78a 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -1,23 +1,10 @@ -use std::{collections::HashSet, env::VarError, str::FromStr}; - use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; +use crate::utils::get_tenants; use leptos::{logging::log, *}; use leptos_router::{use_location, use_navigate, A}; -fn get_from_env_unsafe<F>(name: &str) -> Result<F, VarError> -where - F: FromStr, - <F as FromStr>::Err: std::fmt::Debug, -{ - std::env::var(name) - .map(|val| val.parse().unwrap()) - .map_err(|e| { - return e; - }) -} - #[component] pub fn SideNav() -> impl IntoView { let location = use_location(); @@ -25,16 +12,6 @@ pub fn SideNav() -> impl IntoView { let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); let (app_routes, set_app_routes) = create_signal(create_routes(&tenant_rs.get())); - async fn fetch_tenants(tenants: &str) -> HashSet<String> { - get_from_env_unsafe::<String>(tenants) - .unwrap_or("m,s".into()) - .split(",") - .map(|tenant| tenant.to_string()) - .collect() - } - - let tenants = create_blocking_resource(|| (), move |_| fetch_tenants("TENANTS")); - create_effect(move |_| { let current_path = location.pathname.get(); @@ -93,20 +70,26 @@ pub fn SideNav() -> impl IntoView { class="select w-full max-w-xs shadow-md" > - {match tenants.get() { - Some(tenants_data) => { - let tenants_clone = tenants_data.clone(); - tenants_clone - .iter() - .map(|tenant| { - view! { - <option selected=tenant - == &tenant_rs.get()>{tenant}</option> - } - }) - .collect::<Vec<_>>() + {move || { + let tenants = get_tenants(); + match tenants.is_empty() { + false => { + tenants + .iter() + .map(|tenant| { + view! { + <option selected=tenant + == &tenant_rs.get()>{tenant}</option> + } + }) + .collect::<Vec<_>>() + } + true => { + vec![ + view! { <option disabled=true>{"Loading tenants..."}</option> }, + ] + } } - _ => vec![view! { <option disabled=true>{"Loading tenants..."}</option> }], }} </select> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 320c90090..5f954afe7 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -7,7 +7,7 @@ use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; use crate::pages::fetch_config; -use crate::utils::modal_action; +use crate::utils::{get_host, modal_action}; use leptos::*; use reqwest::StatusCode; use serde_json::{json, Map, Value}; @@ -119,7 +119,7 @@ pub async fn create_context( conditions: Vec<(String, String, String)>, ) -> Result<String, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/context"); let request_payload = construct_request_payload(overrides, conditions); let response = client diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index b6769373b..3bc24833a 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -6,7 +6,7 @@ use crate::components::table::{table::Table, types::Column}; use crate::components::button::button::Button; use crate::components::stat::stat::Stat; use crate::pages::ExperimentList::utils::fetch_default_config; -use crate::utils::modal_action; +use crate::utils::{get_host, modal_action}; use js_sys; use leptos::spawn_local; use leptos::*; @@ -28,7 +28,7 @@ pub async fn create_default_config( pattern: String, ) -> Result<String, String> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/default-config/{key}"); let mut req_body: HashMap<&str, Value> = HashMap::new(); let mut schema: Map<String, Value> = Map::new(); diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index cdb6eb35d..b456ce2cc 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -6,7 +6,7 @@ use crate::components::{ stat::stat::Stat, table::{table::Table, types::Column}, }; -use crate::utils::modal_action; +use crate::utils::{get_host, modal_action}; use leptos::*; use reqwest::StatusCode; use serde_json::{json, Map, Value}; @@ -77,7 +77,7 @@ pub async fn create_dimension( ) -> Result<String, String> { let priority: i64 = priority.parse().unwrap(); let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = get_host(); let url = format!("{host}/dimension"); let mut req_body: HashMap<&str, Value> = HashMap::new(); diff --git a/crates/frontend/src/pages/Dimensions/helper.rs b/crates/frontend/src/pages/Dimensions/helper.rs index ca5e32411..fd596d02c 100644 --- a/crates/frontend/src/pages/Dimensions/helper.rs +++ b/crates/frontend/src/pages/Dimensions/helper.rs @@ -1,16 +1,12 @@ use std::vec::Vec; +use crate::utils::get_host; + use super::types::Dimension; pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = get_host(); let url = format!("{}/dimension", host); let response: Vec<Dimension> = client diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 12096331e..6e4b10e58 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -3,7 +3,6 @@ use leptos::{html::Input, logging::log, *}; use leptos_router::use_params_map; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -use tracing::debug; use web_sys::SubmitEvent; use crate::{ @@ -12,6 +11,7 @@ use crate::{ condition_pills::utils::extract_and_format, table::{table::Table, types::Column}, }, + utils::get_host, }; #[derive( @@ -59,14 +59,15 @@ pub struct Experiment { async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, String> { let client = reqwest::Client::new(); + let host = get_host(); match client - .get(format!("http://localhost:8080/experiments/{}", exp_id)) + .get(format!("{host}/experiments/{}", exp_id)) .header("x-tenant", tenant) .send() .await { Ok(experiment) => { - debug!("experiment response {:?}", experiment); + log!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -82,15 +83,16 @@ async fn ramp_experiment( tenant: &String, ) -> Result<Experiment, String> { let client = reqwest::Client::new(); + let host = get_host(); match client - .patch(format!("http://localhost:8080/experiments/{}/ramp", exp_id)) + .patch(format!("{host}/experiments/{}/ramp", exp_id)) .header("x-tenant", tenant) .json(&json!({ "traffic_percentage": percent })) .send() .await { Ok(experiment) => { - debug!("experiment response {:?}", experiment); + log!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -103,20 +105,19 @@ async fn ramp_experiment( async fn conclude_experiment( exp_id: String, variant_id: String, + tenant: &String, ) -> Result<Experiment, String> { let client = reqwest::Client::new(); + let host = get_host(); match client - .patch(format!( - "http://localhost:8080/experiments/{}/conclude", - exp_id - )) - .header("x-tenant", "mjos") + .patch(format!("{host}/experiments/{}/conclude", exp_id)) + .header("x-tenant", tenant) .json(&json!({ "chosen_variant": variant_id })) .send() .await { Ok(experiment) => { - debug!("experiment response {:?}", experiment); + log!("experiment response {:?}", experiment); Ok(experiment .json::<Experiment>() .await @@ -207,7 +208,7 @@ fn experiment_detail_view( </button> <button class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" - value=&exp.id + value=exp.id.to_string() on:click=move |button_event| { let tenant_clone = tenant_clone.clone(); spawn_local(async move { @@ -454,21 +455,26 @@ fn add_dialogs( <form method="dialog"> {move || { let mut view_arr = vec![]; + let tenant = tenant_rs.get(); for (i, v) in experiment_rs.get().variants.into_iter().enumerate() { let (variant, _) = create_signal(v); + let tenant_clone = tenant.clone(); let view = match variant.get().variant_type { VariantType::CONTROL => { view! { <button class="btn btn-block btn-outline btn-info m-2" - on:click=move |_| spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone()) - .await - .unwrap(); - experiment_ws.refetch(); - }) + on:click=move |_| { + let tenant_clone = tenant_clone.clone(); + spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone(), &tenant_clone) + .await + .unwrap(); + experiment_ws.refetch(); + }) + } > Control @@ -479,14 +485,17 @@ fn add_dialogs( view! { <button class="btn btn-block btn-outline btn-success m-2" - on:click=move |_| spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone()) - .await - .unwrap(); - experiment_ws.refetch(); - }) + on:click=move |_| { + let tenant_clone = tenant_clone.clone(); + spawn_local(async move { + let e = experiment_rs.get(); + let variant = variant.get(); + conclude_experiment(e.id, variant.id.clone(), &tenant_clone) + .await + .unwrap(); + experiment_ws.refetch(); + }) + } > {format!("Variant-{i}")} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 8d74e1006..d8ef8382f 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -2,6 +2,7 @@ use super::types::{DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; use crate::components::{ condition_pills::condition_pills::ContextPills, table::types::Column, }; +use crate::utils::get_host; use core::time::Duration; use leptos::*; use leptos_router::A; @@ -14,13 +15,7 @@ pub async fn fetch_experiments( tenant: &String, ) -> Result<ExperimentsResponse, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = get_host(); let mut query_params = vec![]; if let Some(status) = filters.status { @@ -56,13 +51,7 @@ pub async fn fetch_experiments( pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = get_host(); let url = format!("{}/dimension", host); let response: Vec<Dimension> = client @@ -80,13 +69,7 @@ pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { pub async fn fetch_default_config(tenant: &str) -> Result<Vec<DefaultConfig>, String> { let client = reqwest::Client::new(); - let host = match std::env::var("APP_ENV").as_deref() { - Ok("PROD") => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - Ok("SANDBOX") => "https://context-aware.internal.staging.mum.juspay.net", - _ => "http://localhost:8080", - }; + let host = get_host(); let url = format!("{}/default-config", host); let response: Vec<DefaultConfig> = client diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index f2ecc5cd3..c273e8e40 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -11,11 +11,13 @@ use crate::{ context_form::context_form::ContextForm, }, pages::fetch_config, + utils::get_host, }; async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); - let url = format!("http://localhost:8080/config/resolve?{context}"); + let host = get_host(); + let url = format!("{host}/config/resolve?{context}"); match client .get(url) .query(&[("show_reasoning", "true")]) @@ -112,11 +114,7 @@ pub fn home() -> impl IntoView { "==" => "=", _ => break, // query params do not support the other operators : != and IN, do something differently later }; - context.push(format!( - "{}{op}{}", - dimension.to_lowercase(), - value.to_lowercase() - )); + context.push(format!("{}{op}{}", dimension, value.to_lowercase())); } context.join("&").to_string() }; diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 27bbc7101..2ed892a86 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,5 +1,7 @@ #![allow(non_snake_case)] +use crate::utils::get_host; + use self::types::Config; pub mod ContextOverride; @@ -15,7 +17,8 @@ pub mod types; pub async fn fetch_config(tenant: String) -> Result<Config, String> { let client = reqwest::Client::new(); - let url = "http://localhost:8080/config"; + let host = get_host(); + let url = format!("{host}/config"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { let config: Config = response.json().await.map_err(|e| e.to_string())?; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index 4e2fb6969..2f9b4288b 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -1,5 +1,6 @@ use leptos::{ReadSignal, WriteSignal}; -use std::vec::Vec; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, vec::Vec}; #[derive(Clone, Debug)] pub struct AppRoute { @@ -10,3 +11,30 @@ pub struct AppRoute { } pub type InputVector = Vec<(ReadSignal<String>, WriteSignal<String>)>; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] +pub enum AppEnv { + PROD, + SANDBOX, + DEV, +} + +impl FromStr for AppEnv { + type Err = String; + fn from_str(val: &str) -> Result<AppEnv, Self::Err> { + match val { + "PROD" => Ok(AppEnv::PROD), + "SANDBOX" => Ok(AppEnv::SANDBOX), + "DEV" => Ok(AppEnv::DEV), + _ => Err("invalid app env!!".to_string()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Envs { + pub host: String, + pub app_env: AppEnv, + pub tenants: Vec<String>, +} diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index d6244e056..04b82c127 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,4 +1,4 @@ -#[warn(unused_must_use)] +use crate::types::Envs; use leptos::*; use wasm_bindgen::JsCast; @@ -20,3 +20,19 @@ pub fn modal_action(name: &str, action: &str) { } } } + +pub fn get_host() -> String { + let context = use_context::<Resource<(), Envs>>(); + context + .map_or(None, |resource| resource.get()) + .map(|ctx| ctx.host) + .unwrap_or(String::from("http://localhost:8080")) +} + +pub fn get_tenants() -> Vec<String> { + let context = use_context::<Resource<(), Envs>>(); + context + .map_or(None, |resource| resource.get()) + .map(|ctx| ctx.tenants) + .unwrap_or(vec![]) +} diff --git a/flake.lock b/flake.lock index c9436efe3..72732dde6 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -41,11 +41,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1694081375, - "narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=", + "lastModified": 1698420672, + "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", "owner": "nix-community", "repo": "naersk", - "rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89", + "rev": "aeb58d5e8faead8980a807c840232697982d47b9", "type": "github" }, "original": { @@ -56,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1697379843, - "narHash": "sha256-RcnGuJgC2K/UpTy+d32piEoBXq2M+nVFzM3ah/ZdJzg=", + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "12bdeb01ff9e2d3917e6a44037ed7df6e6c3df9d", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", "type": "github" }, "original": { @@ -70,11 +70,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1697059129, - "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "lastModified": 1704194953, + "narHash": "sha256-RtDKd8Mynhe5CFnVT8s0/0yqtWFMM9LmCzXv/YKxnq4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "rev": "bd645e8668ec6612439a9ee7e71f7eac4099d4f6", "type": "github" }, "original": { @@ -114,11 +114,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1697508761, - "narHash": "sha256-QKWiXUlnke+EiJw3pek1l7xyJ4YsxYXZeQJt/YLgjvA=", + "lastModified": 1704420966, + "narHash": "sha256-oy7QoQ9/5ws9t+S7f4uGqURAKxMdu/CrBJPAXVpFmc0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6f74c92caaf2541641b50ec623676430101d1fd4", + "rev": "9fe08ffa92ee9166c93ad34c27a4792f0c0d58a6", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5cd1c3a37..11f240a6e 100644 --- a/flake.nix +++ b/flake.nix @@ -54,7 +54,6 @@ nodejs_18 leptosfmt wasm-pack - leptosfmt curl ( rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; diff --git a/makefile b/makefile index cf1a4afa9..e17ec0851 100644 --- a/makefile +++ b/makefile @@ -95,20 +95,35 @@ setup: migration env-setup test-tenant dev-tenant kill: -pkill -f target/debug/context-aware-config & +get-password: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh` && echo $$DB_PASSWORD + cac: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ cargo run --color always --bin context-aware-config --no-default-features --features=ssr -run: kill - cd crates/frontend && wasm-pack build --target=web --debug --no-default-features --features=hydrate +frontend: + cd crates/frontend && \ + wasm-pack build --target=web --debug --no-default-features --features=hydrate + cd crates/frontend && \ + npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css + -rm -rf target/site + mkdir target/site && mkdir target/site/pkg + mv crates/frontend/pkg target/site/ + cp -a crates/frontend/assets/. target/site/ + +backend: cargo build --color always + +build: frontend backend + +run: kill build while ! make validate-psql-connection validate-aws-connection; \ do echo "waiting for postgres, localstack bootup"; \ sleep 0.5; \ done cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env - cd crates/frontend && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css make cac -e DOCKER_DNS=$(DOCKER_DNS) ci-test: cleanup ci-setup From 06bcd1a8f8b42855c8038828af9aee2c95a4b16d Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Fri, 12 Jan 2024 10:14:11 +0000 Subject: [PATCH 232/352] chore(version): v0.17.1 [skip ci] --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1ab40fac..b074053b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.17.1 - 2024-01-12 +### Package updates +- context-aware-config bumped to context-aware-config-v0.14.1 +### Global changes +#### Bug Fixes +- frontend build process - (cbdad01) - Shubhranshu Sanjeev +- fixed build failure due to rust-version - (f689597) - Shubhranshu Sanjeev + +- - - + ## v0.17.0 - 2024-01-04 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.9.0 diff --git a/Cargo.lock b/Cargo.lock index d19bcdbad..50676f6e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.14.0" +version = "0.14.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 416199e16..8a5437f85 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.14.1 - 2024-01-12 +#### Bug Fixes +- frontend build process - (cbdad01) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.14.0 - 2024-01-04 #### Bug Fixes - fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index d54ce97a4..64c99d2a3 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.14.0" +version = "0.14.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8a5b1b1153e32bf86e4e58c2e2c83f42eda097d8 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 11 Jan 2024 18:10:22 +0530 Subject: [PATCH 233/352] refactor: using snake case for component fxn names --- .../frontend/src/components/condition_pills/condition_pills.rs | 2 +- crates/frontend/src/components/context_form/context_form.rs | 2 +- .../frontend/src/components/experiment_form/experiment_form.rs | 2 +- crates/frontend/src/components/nav_item/nav_item.rs | 2 +- crates/frontend/src/components/override_form/override_form.rs | 2 +- crates/frontend/src/components/pagination/pagination.rs | 2 +- crates/frontend/src/components/side_nav/side_nav.rs | 2 +- crates/frontend/src/components/stat/stat.rs | 2 +- crates/frontend/src/components/table/table.rs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs index a2ce47012..16a692fe4 100644 --- a/crates/frontend/src/components/condition_pills/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -3,7 +3,7 @@ use leptos::*; use serde_json::Value; #[component] -pub fn ContextPills(context: Value) -> impl IntoView { +pub fn context_pills(context: Value) -> impl IntoView { let condition = extract_and_format(&context); let ctx_values = parse_conditions(condition.clone()); diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 6c445eaa7..a5ad3da79 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use web_sys::MouseEvent; #[component] -pub fn ContextForm<NF>( +pub fn context_form<NF>( handle_change: NF, dimensions: Vec<Dimension>, is_standalone: bool, diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index d68d91477..4a48c06c9 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -44,7 +44,7 @@ struct CombinedResource { } #[component] -pub fn ExperimentForm<NF>( +pub fn experiment_form<NF>( name: String, context: Vec<(String, String, String)>, variants: Vec<Variant>, diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs index 5efd90a60..33a132f6d 100644 --- a/crates/frontend/src/components/nav_item/nav_item.rs +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -2,7 +2,7 @@ use leptos::*; use leptos_router::A; #[component] -pub fn NavItem( +pub fn nav_item( is_active: bool, href: String, text: String, diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 92b81b82e..604942c5c 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use web_sys::MouseEvent; #[component] -pub fn OverrideForm<NF>( +pub fn override_form<NF>( overrides: Map<String, Value>, default_config: Vec<DefaultConfig>, handle_change: NF, diff --git a/crates/frontend/src/components/pagination/pagination.rs b/crates/frontend/src/components/pagination/pagination.rs index cbf0281b8..c8f4db0df 100644 --- a/crates/frontend/src/components/pagination/pagination.rs +++ b/crates/frontend/src/components/pagination/pagination.rs @@ -1,7 +1,7 @@ use leptos::*; #[component] -pub fn Pagination<NF, PF>( +pub fn pagination<NF, PF>( current_page: i64, total_pages: i64, next: NF, diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index e59acb78a..7b98275ee 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -6,7 +6,7 @@ use leptos::{logging::log, *}; use leptos_router::{use_location, use_navigate, A}; #[component] -pub fn SideNav() -> impl IntoView { +pub fn side_nav() -> impl IntoView { let location = use_location(); let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); diff --git a/crates/frontend/src/components/stat/stat.rs b/crates/frontend/src/components/stat/stat.rs index d433497af..5fabe3f7f 100644 --- a/crates/frontend/src/components/stat/stat.rs +++ b/crates/frontend/src/components/stat/stat.rs @@ -1,7 +1,7 @@ use leptos::*; #[component] -pub fn Stat(heading: &'static str, icon: &'static str, number: String) -> impl IntoView { +pub fn stat(heading: &'static str, icon: &'static str, number: String) -> impl IntoView { let icon_class = format!("{} text-5xl", icon); view! { <div class="stats shadow"> diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index ec46defea..bc963d76b 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -18,7 +18,7 @@ fn generate_table_row_str(row: &Value) -> String { } #[component] -pub fn Table( +pub fn table( key_column: String, table_style: String, columns: Vec<Column>, From a297aca9c9e8990c41ed9eb08d863a37b17d22dd Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 11 Jan 2024 18:12:38 +0530 Subject: [PATCH 234/352] fix: fixed dimension form edit flow + fixed table component CellFormatter to accept move closures --- .../dimension_form/dimension_form.rs | 46 +- .../src/components/dimension_form/mod.rs | 2 +- .../src/components/dimension_form/types.rs | 2 +- .../src/components/dimension_form/utils.rs | 20 +- crates/frontend/src/components/mod.rs | 2 + crates/frontend/src/components/modal/mod.rs | 1 + crates/frontend/src/components/modal/modal.rs | 30 ++ crates/frontend/src/components/table/types.rs | 29 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 2 +- .../src/pages/Dimensions/Dimensions.rs | 494 +++++------------- .../src/pages/ExperimentList/utils.rs | 70 ++- crates/frontend/src/utils.rs | 43 ++ 12 files changed, 277 insertions(+), 464 deletions(-) create mode 100644 crates/frontend/src/components/modal/mod.rs create mode 100644 crates/frontend/src/components/modal/modal.rs diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index 44340494d..824f77447 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -1,17 +1,18 @@ use super::types::DimensionCreateReq; -use super::utils::{create_dimension, parse_string_to_json_value_vec}; +use super::utils::create_dimension; use crate::components::button::button::Button; +use crate::utils::parse_string_to_json_value_vec; use leptos::*; use serde_json::json; use web_sys::MouseEvent; #[component] pub fn dimension_form<NF>( - edit: bool, - priority: u16, - dimension_name: String, - dimension_type: String, - dimension_pattern: String, + #[prop(default = false)] edit: bool, + #[prop(default = 0)] priority: u16, + #[prop(default = String::new())] dimension_name: String, + #[prop(default = String::new())] dimension_type: String, + #[prop(default = String::new())] dimension_pattern: String, handle_submit: NF, ) -> impl IntoView where @@ -24,7 +25,7 @@ where let (dimension_type, set_dimension_type) = create_signal(dimension_type); let (dimension_pattern, set_dimension_pattern) = create_signal(dimension_pattern); - let (show_labels, set_show_labels) = create_signal(false); + let (show_labels, set_show_labels) = create_signal(edit); let (error_message, set_error_message) = create_signal("".to_string()); @@ -41,13 +42,13 @@ where "type": f_type.to_string() }) } - "Enum" => { + "enum" => { json!({ "type": "string", "enum": parse_string_to_json_value_vec(f_pattern.as_str()) }) } - "Pattern" => { + "pattern" => { json!({ "type": "string", "pattern": f_pattern.to_string() @@ -73,7 +74,6 @@ where match result { Ok(_) => { handle_submit(); - // modal_action("my_modal_5","close"); } Err(e) => { set_error_message.set(e); @@ -111,17 +111,17 @@ where "number" => { set_dimension_type.set("number".to_string()); } - "Enum" => { - set_dimension_type.set("Enum".to_string()); + "enum" => { + set_dimension_type.set("enum".to_string()); set_dimension_pattern .set(format!("{:?}", vec!["android", "web", "ios"])); } - "Pattern" => { - set_dimension_type.set("Pattern".to_string()); + "pattern" => { + set_dimension_type.set("pattern".to_string()); set_dimension_pattern.set(".*".to_string()); } _ => { - set_dimension_type.set("Other".to_string()); + set_dimension_type.set("other".to_string()); set_dimension_pattern.set("".to_string()); } }; @@ -135,25 +135,25 @@ where <option value="number" - selected=move || { dimension_type.get() == "number".to_string() } + selected=move || { logging::log!("{:?}", dimension_type.get()); dimension_type.get() == "number".to_string() } > "Number" </option> <option - value="Enum" - selected=move || { dimension_type.get() == "Enum".to_string() } + value="enum" + selected=move || { dimension_type.get() == "enum".to_string() } > "String (Enum)" </option> <option - value="Pattern" - selected=move || { dimension_type.get() == "Pattern".to_string() } + value="pattern" + selected=move || { dimension_type.get() == "pattern".to_string() } > "String (regex)" </option> <option - value="Other" - selected=move || { dimension_type.get() == "Other".to_string() } + value="other" + selected=move || { dimension_type.get() == "other".to_string() } > "Other" </option> @@ -245,4 +245,4 @@ where </form> } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/dimension_form/mod.rs b/crates/frontend/src/components/dimension_form/mod.rs index f6e5d65d2..3c93972ac 100644 --- a/crates/frontend/src/components/dimension_form/mod.rs +++ b/crates/frontend/src/components/dimension_form/mod.rs @@ -1,3 +1,3 @@ pub mod dimension_form; +pub mod types; pub mod utils; -pub mod types; \ No newline at end of file diff --git a/crates/frontend/src/components/dimension_form/types.rs b/crates/frontend/src/components/dimension_form/types.rs index 7b87b6ccd..ecf60201a 100644 --- a/crates/frontend/src/components/dimension_form/types.rs +++ b/crates/frontend/src/components/dimension_form/types.rs @@ -6,4 +6,4 @@ pub struct DimensionCreateReq { pub dimension: String, pub priority: u16, pub schema: Value, -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs index 652a0c2e7..6ada5a366 100644 --- a/crates/frontend/src/components/dimension_form/utils.rs +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -1,22 +1,6 @@ use super::types::DimensionCreateReq; -use leptos::logging; -use reqwest::StatusCode; -use serde_json::Value; use crate::utils::get_host; - -pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { - // Parse the input string into a serde_json::Value - let parsed = serde_json::from_str::<Value>(input); - - // Ensure the Value is an Array and convert it to Vec<Value> - match parsed { - Ok(Value::Array(arr)) => arr, - _ => { - logging::log!("Not a valid json in the input"); - vec![] - } - } -} +use reqwest::StatusCode; pub async fn create_dimension( tenant: String, @@ -40,4 +24,4 @@ pub async fn create_dimension( StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), _ => Err("Internal Server Error".to_string()), } -} \ No newline at end of file +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index a01a9d667..1289750a5 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,7 +1,9 @@ pub mod button; pub mod condition_pills; pub mod context_form; +pub mod dimension_form; pub mod experiment_form; +pub mod modal; pub mod nav_item; pub mod override_form; pub mod pagination; diff --git a/crates/frontend/src/components/modal/mod.rs b/crates/frontend/src/components/modal/mod.rs new file mode 100644 index 000000000..6738a0f2f --- /dev/null +++ b/crates/frontend/src/components/modal/mod.rs @@ -0,0 +1 @@ +pub mod modal; diff --git a/crates/frontend/src/components/modal/modal.rs b/crates/frontend/src/components/modal/modal.rs new file mode 100644 index 000000000..402b77390 --- /dev/null +++ b/crates/frontend/src/components/modal/modal.rs @@ -0,0 +1,30 @@ +use leptos::*; + +#[component] +pub fn modal<NF>( + id: String, + #[prop(default = String::new())] classnames: String, + #[prop(default = String::new())] heading: String, + handle_close: NF, + children: Children, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let classnames = format!("modal modal-middle {classnames}"); + view! { + <dialog id=id class=classnames> + <div class="modal-box"> + <button + class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" + on:click=move |_| { handle_close() } + > + <i class="ri-close-line"></i> + </button> + <h3 class="font-bold text-lg">{heading}</h3> + {children()} + <div class="modal-action"></div> + </div> + </dialog> + } +} diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs index d5dc4478d..a1dfbea76 100644 --- a/crates/frontend/src/components/table/types.rs +++ b/crates/frontend/src/components/table/types.rs @@ -1,20 +1,28 @@ +use std::rc::Rc; + use leptos::{view, IntoView, View}; use serde_json::{Map, Value}; -pub type CellFormatter = fn(&str, &Map<String, Value>) -> View; +pub type CellFormatter = Box<Rc<dyn Fn(&str, &Map<String, Value>) -> View>>; #[derive(Clone, Debug)] pub struct TableSettings { pub redirect_prefix: Option<String>, } -#[derive(Clone, PartialEq)] +#[derive(Clone)] pub struct Column { pub name: String, pub hidden: bool, pub formatter: CellFormatter, } +impl PartialEq for Column { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.hidden == other.hidden + } +} + fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { view! { <span>{value.to_string()}</span> }.into_view() } @@ -22,20 +30,19 @@ fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { impl Column { pub fn default(name: String) -> Column { Column { - name: name, + name, hidden: false, - formatter: default_formatter, + formatter: Box::new(Rc::new(default_formatter)), } } - pub fn new( - name: String, - hidden: Option<bool>, - formatter: Option<CellFormatter>, - ) -> Column { + pub fn new<NF>(name: String, hidden: Option<bool>, formatter: NF) -> Column + where + NF: Fn(&str, &Map<String, Value>) -> View + 'static, + { Column { - name: name, + name, hidden: hidden.unwrap_or(false), - formatter: formatter.unwrap_or(default_formatter), + formatter: Box::new(Rc::new(formatter)), } } } diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 3bc24833a..d2c2eaa1c 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -350,7 +350,7 @@ pub fn DefaultConfig() -> impl IntoView { Column::default("value".to_string()), Column::default("created_at".to_string()), Column::default("created_by".to_string()), - Column::new("EDIT".to_string(), None, Some(custom_formatter)), + Column::new("EDIT".to_string(), None, custom_formatter), ] }); diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index b456ce2cc..2636d7d14 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,379 +1,29 @@ use std::collections::HashMap; -use std::rc::Rc; use crate::components::button::button::Button; +use crate::components::dimension_form::dimension_form::DimensionForm; +use crate::components::modal::modal::Modal; use crate::components::{ stat::stat::Stat, table::{table::Table, types::Column}, }; -use crate::utils::{get_host, modal_action}; +use crate::utils::{close_modal, show_modal}; use leptos::*; -use reqwest::StatusCode; use serde_json::{json, Map, Value}; -use web_sys::MouseEvent; use crate::pages::Dimensions::helper::fetch_dimensions; #[derive(Clone, Debug, Default)] pub struct RowData { pub dimension: String, - pub priority: String, + pub priority: u16, pub type_: String, pub pattern: String, } -fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { - // Parse the input string into a serde_json::Value - let parsed = serde_json::from_str::<Value>(input); - - // Ensure the Value is an Array and convert it to Vec<Value> - match parsed { - Ok(Value::Array(arr)) => arr, - _ => { - logging::log!("Not a valid json in the input"); - vec![] - } - } -} - -pub fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { - let global_signal = use_context::<RwSignal<RowData>>().unwrap(); - let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); - let row_priority = row["priority"].clone().to_string().replace("\"", ""); - - let schema = row["schema"].clone().to_string(); - let schema_object = serde_json::from_str::<serde_json::Value>(&schema).unwrap(); - let edit_signal = use_context::<RwSignal<bool>>().unwrap(); - - let row_type = schema_object.get("type").unwrap().to_string(); - let row_pattern = schema_object - .get("pattern") - .unwrap_or(&Value::String("".to_string())) - .to_string(); - - let edit_click_handler = move |_| { - let row_data = RowData { - dimension: row_dimension.clone(), - priority: row_priority.clone(), - type_: row_type.clone(), - pattern: row_pattern.clone(), - }; - global_signal.set(row_data); - edit_signal.set(true); - js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); - }; - - let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; - - view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() -} - -pub async fn create_dimension( - tenant: String, - key: String, - priority: String, - key_type: String, - pattern: String, -) -> Result<String, String> { - let priority: i64 = priority.parse().unwrap(); - let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/dimension"); - - let mut req_body: HashMap<&str, Value> = HashMap::new(); - let mut schema: Map<String, Value> = Map::new(); - - match key_type.as_str() { - "number" => { - schema.insert("type".to_string(), Value::String(key_type.clone())); - } - "Enum" => { - let array = parse_string_to_json_value_vec(pattern.clone().as_str()); - schema.insert("type".to_string(), Value::String("string".to_string())); - schema.insert("enum".to_string(), Value::Array(array)); - } - "Pattern" => { - schema.insert("type".to_string(), Value::String("string".to_string())); - schema.insert("pattern".to_string(), Value::String(pattern.clone())); - } - _ => { - let json_pattern = serde_json::from_str::<Value>(&pattern.clone()) - .map_err(|e| e.to_string())?; - req_body.insert("schema", json_pattern); - } - } - - req_body.insert("dimension", Value::String(key)); - req_body.insert("priority", Value::Number(priority.into())); - req_body.insert("schema", Value::Object(schema)); - - let response = client - .put(url) - .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") - .json(&req_body) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), - _ => Err("Internal Server Error".to_string()), - } -} - -#[component] -fn ModalComponent( - handle_submit: Rc<dyn Fn()>, - tenant: ReadSignal<String>, -) -> impl IntoView { - view! { - <div class="pt-4"> - <FormComponent handle_submit=handle_submit tenant=tenant/> - </div> - } -} - -#[component] -fn FormComponent( - handle_submit: Rc<dyn Fn()>, - tenant: ReadSignal<String>, -) -> impl IntoView { - use leptos::html::Input; - use leptos::html::Textarea; - let handle_submit = handle_submit.clone(); - let global_state = use_context::<RwSignal<RowData>>(); - let row_data = global_state.unwrap().get(); - - let (dimension, set_dimension) = create_signal(row_data.dimension); - let (priority, set_priority) = create_signal(row_data.priority); - let (keytype, set_keytype) = create_signal(row_data.type_); - let (pattern, set_pattern) = create_signal(row_data.pattern); - let (show_labels, set_show_labels) = create_signal(false); - let edit_signal = use_context::<RwSignal<bool>>(); - - create_effect(move |_| { - if let Some(row_data) = global_state { - logging::log!("default config create effect"); - if edit_signal.unwrap().get() == true { - set_dimension.set(row_data.get().dimension.clone().to_string()); - set_priority.set(row_data.get().priority.clone()); - set_keytype.set(row_data.get().type_.clone().to_string()); - set_pattern.set(row_data.get().pattern.clone()); - } else { - set_dimension.set("".to_string()); - set_priority.set("".to_string()); - set_keytype.set("".to_string()); - set_pattern.set("".to_string()); - } - } - }); - - let input_element: NodeRef<Input> = create_node_ref(); - let input_element_two: NodeRef<Input> = create_node_ref(); - let input_element_three: NodeRef<Textarea> = create_node_ref(); - let (error_message, set_error_message) = create_signal("".to_string()); - - let on_submit = { - let handle_submit = handle_submit.clone(); - move |ev: MouseEvent| { - ev.prevent_default(); - - let value1 = input_element.get().expect("<input> to exist").value(); - let value2 = input_element_two.get().expect("<input> to exist").value(); - let value3 = input_element_three.get().expect("<input> to exist").value(); - - set_dimension.set(value1.clone()); - set_priority.set(value2.clone()); - set_pattern.set(value3.clone()); - let handle_submit_clone = handle_submit.clone(); - - spawn_local({ - let handle_submit = handle_submit_clone; - async move { - let result = create_dimension( - tenant.get(), - dimension.get(), - priority.get(), - keytype.get(), - pattern.get(), - ) - .await; - - match result { - Ok(_) => { - handle_submit(); - // modal_action("my_modal_5","close"); - } - Err(e) => { - set_error_message.set(e); - // Handle error - // Consider logging or displaying the error - } - } - } - }); - } - }; - - view! { - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill" onclick="my_modal_5.close()"></i> - </button> - </form> - <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Dimension</span> - </label> - <input - disabled=move || { edit_signal.unwrap().get() } - type="text" - placeholder="Dimension" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=dimension - node_ref=input_element - /> - </div> - <select - name="schemaType[]" - on:change=move |ev| { - set_show_labels.set(true); - match event_target_value(&ev).as_str() { - "number" => { - set_keytype.set("number".to_string()); - } - "Enum" => { - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - } - "Pattern" => { - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - } - _ => { - set_keytype.set("Other".to_string()); - set_pattern.set("".to_string()); - } - }; - } - - class="select select-bordered" - > - <option disabled selected> - Set Schema - </option> - - <option - value="number" - selected=move || { keytype.get() == "number".to_string() } - > - "Number" - </option> - <option - value="Enum" - selected=move || { keytype.get() == "Enum".to_string() } - > - "String (Enum)" - </option> - <option - value="Pattern" - selected=move || { keytype.get() == "Pattern".to_string() } - > - "String (regex)" - </option> - <option - value="Other" - selected=move || { keytype.get() == "Other".to_string() } - > - "Other" - </option> - </select> - - {move || { - view! { - <Show when=move || (keytype.get() == "number")> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> - Priority - </span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> - </Show> - - <Show when=move || (show_labels.get() && (keytype.get() != "number"))> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> - Priority - </span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority - node_ref=input_element_two - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> - {keytype.get()} - </span> - </label> - <textarea - type="text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - - node_ref=input_element_three - > - {pattern.get()} - </textarea> - - </div> - </Show> - } - }} - - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click=on_submit/> - </div> - - { - view! { - <div> - <p class="text-red-500">{move || error_message.get()}</p> - </div> - } - } - - </form> - </div> - </dialog> - } -} - #[component] pub fn Dimensions() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let global_state = create_rw_signal(RowData::default()); - provide_context(global_state); - - let edit_signal = create_rw_signal(true); - provide_context(edit_signal); let (open_form, set_open_form) = create_signal(false); let dimensions = create_blocking_resource( @@ -386,14 +36,96 @@ pub fn Dimensions() -> impl IntoView { }, ); + let selected_dimension = create_rw_signal::<Option<RowData>>(None); + let table_columns = create_memo(move |_| { + let edit_col_formatter = move |_: &str, row: &Map<String, Value>| { + let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); + let row_priority_str = row["priority"].clone().to_string().replace("\"", ""); + let row_priority = match row_priority_str.parse::<u16>() { + Ok(val) => val, + Err(_) => 0 as u16, + }; + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) + .unwrap_or(HashMap::new()); + + let pattern_or_enum = schema_object + .keys() + .find(|key| { + key.to_string() == "pattern".to_string() + || key.to_string() == "enum".to_string() + }) + .and_then(|val| Some(val.clone())) + .unwrap_or(String::new()); + + let row_type = match schema_object.get("type") { + Some(Value::String(type_)) if type_ == "string" => { + pattern_or_enum.clone() + } + Some(Value::String(type_)) if type_ == "number" => type_.clone(), + Some(Value::String(_)) => String::from("other"), + Some(_) | None => String::new(), + }; + + let row_pattern = match schema_object.get("type") { + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "string" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| Some(val.clone().to_string())) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "enum" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| { + if let Value::Array(v) = val { + return format!( + "[{}]", + v.iter() + .map(|v| v.to_string()) + .collect::<Vec<String>>() + .join(",") + ) + .into(); + } + None + }) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) if type_ == "number" => String::new(), + Some(Value::String(_)) => schema, + Some(_) | None => String::new(), + }; + + let edit_click_handler = move |_| { + let row_data = RowData { + dimension: row_dimension.clone(), + priority: row_priority.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + }; + selected_dimension.set(Some(row_data)); + set_open_form.set(true); + show_modal("dimension_form_modal"); + }; + + let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + + view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() + }; vec![ Column::default("dimension".to_string()), Column::default("priority".to_string()), Column::default("schema".to_string()), Column::default("created_by".to_string()), Column::default("created_at".to_string()), - Column::new("EDIT".to_string(), None, Some(custom_formatter)), + Column::new("EDIT".to_string(), None, edit_col_formatter), ] }); @@ -401,7 +133,6 @@ pub fn Dimensions() -> impl IntoView { <div class="p-8"> <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> <div class="pb-4"> - {move || { let value = dimensions.get(); let total_items = match value { @@ -413,14 +144,41 @@ pub fn Dimensions() -> impl IntoView { } }} <Show when=move || { open_form.get() }> - <ModalComponent - handle_submit=Rc::new(move || { - set_open_form.set(false); - dimensions.refetch() - }) + <Modal + id="dimension_form_modal".to_string() + handle_close=move || { + close_modal("dimension_form_modal"); + selected_dimension.set(None); + } + > + + {move || { + if let Some(selected_dimension_data) = selected_dimension.get() { + view! { + <DimensionForm + edit=true + priority=selected_dimension_data.priority + dimension_name=selected_dimension_data.dimension + dimension_type=selected_dimension_data.type_ + dimension_pattern=selected_dimension_data.pattern + handle_submit=move || { + set_open_form.set(false); + dimensions.refetch(); + selected_dimension.set(None); + } + /> + } + } else { + view! { + <DimensionForm handle_submit=move || { + set_open_form.set(false); + dimensions.refetch() + }/> + } + } + }} - tenant=tenant_rs - /> + </Modal> </Show> </div> @@ -430,13 +188,9 @@ pub fn Dimensions() -> impl IntoView { <h2 class="card-title">Dimensions</h2> <Button text="Create Dimension".to_string() - on_click={ - let edit_clone = edit_signal.to_owned(); - move |_| { - edit_clone.set(false); - set_open_form.set(true); - modal_action("my_modal_5", "open"); - } + on_click=move |_| { + set_open_form.set(true); + show_modal("dimension_form_modal"); } /> diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index d8ef8382f..2b526f0f6 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -90,7 +90,7 @@ pub fn experiment_table_columns() -> Vec<Column> { Column::new( "name".to_string(), None, - Some(|value: &str, row: &Map<String, Value>| { + |value: &str, row: &Map<String, Value>| { let (copied, set_copied) = create_signal(false); let experiment_name = value.to_string(); @@ -137,33 +137,29 @@ pub fn experiment_table_columns() -> Vec<Column> { </div> } .into_view() - }), - ), - Column::new( - "status".to_string(), - None, - Some(|value: &str, _| { - let badge_color = match value { - "CREATED" => "badge-info", - "INPROGRESS" => "badge-warning", - "CONCLUDED" => "badge-success", - &_ => "info", - }; - let class = format!("badge {}", badge_color); - view! { - <div class={class}> - <span class="text-white font-semibold text-xs"> - {value.to_string()} - </span> - </div> - } - .into_view() - }), + }, ), + Column::new("status".to_string(), None, |value: &str, _| { + let badge_color = match value { + "CREATED" => "badge-info", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-success", + &_ => "info", + }; + let class = format!("badge {}", badge_color); + view! { + <div class={class}> + <span class="text-white font-semibold text-xs"> + {value.to_string()} + </span> + </div> + } + .into_view() + }), Column::new( "context".to_string(), None, - Some(|_, row: &Map<String, Value>| { + |_, row: &Map<String, Value>| { let context = match row.get("context") { Some(value) => value.to_owned(), None => json!(""), @@ -175,23 +171,19 @@ pub fn experiment_table_columns() -> Vec<Column> { </div> } .into_view() - }), + }, ), - Column::new( - "chosen_variant".to_string(), - None, - Some(|value: &str, _| { - let label = match value { - "null" => "¯\\_(ツ)_/¯".to_string(), - other => other.to_string(), - }; + Column::new("chosen_variant".to_string(), None, |value: &str, _| { + let label = match value { + "null" => "¯\\_(ツ)_/¯".to_string(), + other => other.to_string(), + }; - view! { - <span>{label}</span> - } - .into_view() - }), - ), + view! { + <span>{label}</span> + } + .into_view() + }), Column::default("created_at".to_string()), Column::default("created_by".to_string()), Column::default("last_modified".to_string()), diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index 04b82c127..b1ae71421 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,5 +1,6 @@ use crate::types::Envs; use leptos::*; +use serde_json::Value; use wasm_bindgen::JsCast; pub fn modal_action(name: &str, action: &str) { @@ -36,3 +37,45 @@ pub fn get_tenants() -> Vec<String> { .map(|ctx| ctx.tenants) .unwrap_or(vec![]) } + +pub fn get_element_by_id<T>(id: &'static str) -> Option<T> +where + T: wasm_bindgen::JsCast + Clone, +{ + let option_dom_ele = document().get_element_by_id(id); + logging::log!("DOM element found {:?}", option_dom_ele); + match option_dom_ele { + Some(dom_ele) => dom_ele.dyn_ref::<T>().cloned(), + None => None, + } +} + +pub fn show_modal(id: &'static str) { + let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); + if let Some(dialog_ele) = option_dialog_ele { + let _ = dialog_ele.show_modal(); + logging::log!("{:?}", dialog_ele); + } +} + +pub fn close_modal(id: &'static str) { + let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); + if let Some(dialog_ele) = option_dialog_ele { + let _ = dialog_ele.close(); + logging::log!("{:?}", dialog_ele); + } +} + +pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { + // Parse the input string into a serde_json::Value + let parsed = serde_json::from_str::<Value>(input); + + // Ensure the Value is an Array and convert it to Vec<Value> + match parsed { + Ok(Value::Array(arr)) => arr, + _ => { + logging::log!("Not a valid json in the input"); + vec![] + } + } +} From dfd8abb2c700d636d8506c21b67b4c6b3726853d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 11 Jan 2024 20:47:31 +0530 Subject: [PATCH 235/352] fix: refactored DefaultConfig component + fixed edit flow --- .../default_config_form.rs | 235 +++++++++ .../src/components/default_config_form/mod.rs | 3 + .../components/default_config_form/types.rs | 8 + .../components/default_config_form/utils.rs | 28 ++ crates/frontend/src/components/mod.rs | 1 + .../src/pages/DefaultConfig/DefaultConfig.rs | 448 +++++------------- .../src/pages/Dimensions/Dimensions.rs | 5 +- 7 files changed, 402 insertions(+), 326 deletions(-) create mode 100644 crates/frontend/src/components/default_config_form/default_config_form.rs create mode 100644 crates/frontend/src/components/default_config_form/mod.rs create mode 100644 crates/frontend/src/components/default_config_form/types.rs create mode 100644 crates/frontend/src/components/default_config_form/utils.rs diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs new file mode 100644 index 000000000..73f58bbb5 --- /dev/null +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -0,0 +1,235 @@ +use leptos::*; +use serde_json::{json, Value}; +use web_sys::MouseEvent; + +use crate::{components::button::button::Button, utils::parse_string_to_json_value_vec}; + +use super::{types::DefaultConfigCreateReq, utils::create_default_config}; + +#[component] +pub fn default_config_form<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] config_key: String, + #[prop(default = String::new())] config_type: String, + #[prop(default = String::new())] config_pattern: String, + #[prop(default = String::new())] config_value: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let (config_key, set_config_key) = create_signal(config_key); + let (config_type, set_config_type) = create_signal(config_type); + let (config_pattern, set_config_pattern) = create_signal(config_pattern); + let (config_value, set_config_value) = create_signal(config_value); + + let (show_labels, set_show_labels) = create_signal(edit); + + let (error_message, set_error_message) = create_signal("".to_string()); + + let on_submit = move |ev: MouseEvent| { + ev.prevent_default(); + let f_name = config_key.get(); + let f_type = config_type.get(); + let f_pattern = config_pattern.get(); + let f_value = config_value.get(); + + let f_value = match f_type.as_str() { + "number" => Value::Number(f_value.parse::<i32>().unwrap().into()), + _ => Value::String(f_value), + }; + + let f_schema = match f_type.as_str() { + "number" => { + json!({ + "type": f_type.to_string(), + }) + } + "enum" => { + json!({ + "type": "string", + "enum": parse_string_to_json_value_vec(f_pattern.as_str()) + }) + } + "pattern" => { + json!({ + "type": "string", + "pattern": f_pattern.to_string() + }) + } + _ => { + json!(f_pattern.to_string()) + } + }; + + let payload = DefaultConfigCreateReq { + schema: f_schema, + value: f_value, + }; + + let handle_submit_clone = handle_submit.clone(); + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_default_config( + f_name.clone(), + tenant_rs.get(), + payload.clone(), + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(e) => { + set_error_message.set(e); + // Handle error + // Consider logging or displaying the error + } + } + } + }); + }; + view! { + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Key</span> + </label> + <input + disabled=edit + type="text" + placeholder="Key" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=config_key.get() + on:change=move |ev| { + let value = event_target_value(&ev); + set_config_key.set(value); + } + /> + </div> + + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_config_type.set("number".to_string()); + } + "enum" => { + set_config_type.set("enum".to_string()); + set_config_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "pattern" => { + set_config_type.set("pattern".to_string()); + set_config_pattern.set(".*".to_string()); + } + _ => { + set_config_type.set("other".to_string()); + set_config_pattern.set("".to_string()); + } + }; + } + + class="select select-bordered" + > + <option disabled selected> + Set Schema + </option> + + <option value="number" selected=move || { config_type.get() == "number".to_string() }> + "Number" + </option> + <option value="enum" selected=move || { config_type.get() == "enum".to_string() }> + "String (Enum)" + </option> + <option value="pattern" selected=move || { config_type.get() == "pattern".to_string() }> + "String (regex)" + </option> + <option value="other" selected=move || { config_type.get() == "other".to_string() }> + "Other" + </option> + </select> + + {move || { + view! { + <Show when=move || (config_type.get() == "number")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="number" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=config_value.get() + on:change=move |ev| { + logging::log!( + "{:?}", event_target_value(&ev) + ); + set_config_value.set(event_target_value(&ev)); + } + /> + </div> + </Show> + + <Show when=move || (show_labels.get() && (config_type.get() != "number"))> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono">Value</span> + </label> + <input + type="text" + placeholder="Value" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=config_value.get() + on:change=move |ev| { + logging::log!( + "{:?}", event_target_value(&ev) + ); + set_config_value.set(event_target_value(&ev)); + } + /> + </div> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {config_type.get()} + </span> + </label> + <textarea + type="text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_config_pattern.set(value); + } + > + {config_pattern.get()} + </textarea> + + </div> + </Show> + } + }} + + <div class="form-control mt-6"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + { + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } + + </form> + } +} diff --git a/crates/frontend/src/components/default_config_form/mod.rs b/crates/frontend/src/components/default_config_form/mod.rs new file mode 100644 index 000000000..8a08759dc --- /dev/null +++ b/crates/frontend/src/components/default_config_form/mod.rs @@ -0,0 +1,3 @@ +pub mod default_config_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/default_config_form/types.rs b/crates/frontend/src/components/default_config_form/types.rs new file mode 100644 index 000000000..b95655b4f --- /dev/null +++ b/crates/frontend/src/components/default_config_form/types.rs @@ -0,0 +1,8 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize, Clone)] +pub struct DefaultConfigCreateReq { + pub schema: Value, + pub value: Value, +} diff --git a/crates/frontend/src/components/default_config_form/utils.rs b/crates/frontend/src/components/default_config_form/utils.rs new file mode 100644 index 000000000..a061ea84a --- /dev/null +++ b/crates/frontend/src/components/default_config_form/utils.rs @@ -0,0 +1,28 @@ +use super::types::DefaultConfigCreateReq; +use crate::utils::get_host; +use reqwest::StatusCode; + +pub async fn create_default_config( + key: String, + tenant: String, + payload: DefaultConfigCreateReq, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/default-config/{key}"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 1289750a5..68e0b1c80 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -1,6 +1,7 @@ pub mod button; pub mod condition_pills; pub mod context_form; +pub mod default_config_form; pub mod dimension_form; pub mod experiment_form; pub mod modal; diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index d2c2eaa1c..751bf227e 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -1,337 +1,26 @@ use std::collections::HashMap; -use std::rc::Rc; +use crate::components::default_config_form::default_config_form::DefaultConfigForm; +use crate::components::modal::modal::Modal; use crate::components::table::{table::Table, types::Column}; use crate::components::button::button::Button; use crate::components::stat::stat::Stat; use crate::pages::ExperimentList::utils::fetch_default_config; -use crate::utils::{get_host, modal_action}; -use js_sys; -use leptos::spawn_local; +use crate::utils::{close_modal, show_modal}; use leptos::*; -use reqwest::StatusCode; use serde_json::{json, Map, Value}; -use web_sys::MouseEvent; #[derive(Clone, Debug, Default)] pub struct RowData { pub key: String, pub value: String, -} - -pub async fn create_default_config( - tenant: String, - key: String, - value: String, - key_type: String, - pattern: String, -) -> Result<String, String> { - let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/default-config/{key}"); - let mut req_body: HashMap<&str, Value> = HashMap::new(); - let mut schema: Map<String, Value> = Map::new(); - match key_type.as_str() { - "number" => { - schema.insert("type".to_string(), Value::String(key_type.clone())); - req_body.insert( - "value", - Value::Number(value.clone().parse::<i32>().unwrap().into()), - ); - } - // "Enum" => { - // let array = parse_string_to_json_value_vec(pattern.clone().as_str()); - // schema.insert("type".to_string(),Value::String("string".to_string())); - // schema.insert("enum".to_string(), Value::Array(array)); - // req_body.insert("value", Value::Array(value2)); - // }, - "Pattern" | "Enum" => { - schema.insert("type".to_string(), Value::String("string".to_string())); - schema.insert("pattern".to_string(), Value::String(pattern.clone())); - } - _ => { - let json_pattern = serde_json::from_str::<Value>(&pattern.clone()) - .map_err(|e| e.to_string())?; - req_body.insert("schema", json_pattern); - } - } - - if key_type != "number".to_string() { - req_body.insert("value", Value::String(value)); - } - - req_body.insert("schema", Value::Object(schema)); - - let response = client - .put(url) - .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") - .json(&req_body) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), - _ => Err("Internal Server Error".to_string()), - } -} - -#[component] -fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { - view! { - <dialog id="my_modal_5" class="modal modal-bottom sm:modal-middle"> - <div class="modal-box relative bg-white"> - <form method="dialog" class="flex justify-end"> - <button> - <i class="ri-close-fill"></i> - </button> - </form> - <FormComponent handle_submit=handle_submit/> - </div> - </dialog> - } -} - -#[component] -fn FormComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { - use leptos::html::Input; - use leptos::html::Textarea; - let handle_submit = handle_submit.clone(); - let global_state = use_context::<RwSignal<RowData>>(); - let _row_data = global_state.unwrap().get(); - - let (key, set_key) = create_signal("".to_string()); - let (value, set_value) = create_signal("".to_string()); - let (keytype, set_keytype) = create_signal("".to_string()); - let (pattern, set_pattern) = create_signal("".to_string()); - - let edit_signal = use_context::<RwSignal<bool>>(); - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - - let (show_labels, set_show_labels) = create_signal(false); - - create_effect(move |_| { - if let Some(row_data) = global_state { - logging::log!("default config create effect"); - if edit_signal.unwrap().get() == true { - set_key.set(row_data.get().key.clone().to_string()); - set_value.set(row_data.get().value.clone().to_string()); - } else { - set_key.set("".to_string()); - set_value.set("".to_string()); - } - } - }); - - let input_element: NodeRef<Input> = create_node_ref(); - let input_element_two: NodeRef<Input> = create_node_ref(); - let input_element_three: NodeRef<Textarea> = create_node_ref(); - - let (error_message, set_error_message) = create_signal("".to_string()); - - let on_submit = { - let handle_submit = handle_submit.clone(); - move |ev: MouseEvent| { - ev.prevent_default(); - - let current_tenant = tenant_rs.get(); - let value1 = input_element.get().expect("<input> to exist").value(); - let value2 = input_element_two.get().expect("<input> to exist").value(); - let value3 = input_element_three.get().expect("<input> to exist").value(); - - set_key.set(value1.clone()); - set_value.set(value2.clone()); - set_pattern.set(value3.clone()); - - let handle_submit_clone = handle_submit.clone(); - - spawn_local({ - let handle_submit = handle_submit_clone; - async move { - let result = create_default_config( - current_tenant, - key.get(), - value.get(), - keytype.get(), - pattern.get(), - ) - .await; - - match result { - Ok(_) => { - handle_submit(); - modal_action("my_modal_5", "close"); - } - Err(e) => { - set_error_message.set(e); - // Handle error - // Consider logging or displaying the error - } - } - } - }); - } - }; - - view! { - <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Key</span> - </label> - <input - disabled=move || { edit_signal.unwrap().get() } - type="text" - placeholder="Key" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=key - node_ref=input_element - /> - </div> - - <select - name="schemaType[]" - on:change=move |ev| { - set_show_labels.set(true); - match event_target_value(&ev).as_str() { - "number" => { - set_keytype.set("number".to_string()); - } - "Enum" => { - set_keytype.set("Enum".to_string()); - set_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); - } - "Pattern" => { - set_keytype.set("Pattern".to_string()); - set_pattern.set(".*".to_string()); - } - _ => { - set_keytype.set("Other".to_string()); - set_pattern.set("".to_string()); - } - }; - } - - class="select select-bordered" - > - <option disabled selected> - Set Schema - </option> - - <option value="number" selected=move || { keytype.get() == "number".to_string() }> - "Number" - </option> - <option value="Enum" selected=move || { keytype.get() == "Enum".to_string() }> - "String (Enum)" - </option> - <option value="Pattern" selected=move || { keytype.get() == "Pattern".to_string() }> - "String (regex)" - </option> - <option value="Other" selected=move || { keytype.get() == "Other".to_string() }> - "Other" - </option> - </select> - - {move || { - view! { - <Show when=move || (keytype.get() == "number")> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> - </label> - <input - type="number" - placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=value - node_ref=input_element_two - /> - </div> - </Show> - - <Show when=move || (show_labels.get() && (keytype.get() != "number"))> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> - </label> - <input - type="text" - placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=value - node_ref=input_element_two - /> - </div> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> - {keytype.get()} - </span> - </label> - <textarea - type="text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - - node_ref=input_element_three - > - {pattern.get()} - </textarea> - - </div> - </Show> - } - }} - - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click=on_submit/> - </div> - - { - view! { - <div> - <p class="text-red-500">{move || error_message.get()}</p> - </div> - } - } - - </form> - } -} - -fn custom_formatter(_value: &str, row: &Map<String, Value>) -> View { - let global_signal = use_context::<RwSignal<RowData>>().unwrap(); - let row_key = row["key"].clone().to_string().replace("\"", ""); - let row_value = row["value"].clone().to_string().replace("\"", ""); - - let edit_signal = use_context::<RwSignal<bool>>().unwrap(); - let edit_click_handler = move |_| { - let row_data = RowData { - key: row_key.clone(), - value: row_value.clone(), - }; - edit_signal.set(true); - global_signal.set(row_data); - js_sys::eval("document.getElementById('my_modal_5').showModal();").unwrap(); - }; - - let edit_icon: HtmlElement<html::I> = - view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; - - view! { <span on:click=edit_click_handler>{edit_icon}</span> }.into_view() + pub pattern: String, + pub type_: String, } #[component] pub fn DefaultConfig() -> impl IntoView { - // let (edit_row_data, set_edit_row_data) = create_signal(None); - let global_state = create_rw_signal(RowData::default()); - provide_context(global_state); - - let edit_signal = create_rw_signal(true); - provide_context(edit_signal); - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let default_config_resource = create_blocking_resource( move || tenant_rs.get(), @@ -343,30 +32,143 @@ pub fn DefaultConfig() -> impl IntoView { }, ); + let selected_config = create_rw_signal::<Option<RowData>>(None); + let table_columns = create_memo(move |_| { + let edit_col_formatter = move |_: &str, row: &Map<String, Value>| { + logging::log!("{:?}", row); + let row_key = row["key"].clone().to_string().replace("\"", ""); + let row_value = row["value"].clone().to_string().replace("\"", ""); + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) + .unwrap_or(HashMap::new()); + + let pattern_or_enum = schema_object + .keys() + .find(|key| { + key.to_string() == "pattern".to_string() + || key.to_string() == "enum".to_string() + }) + .and_then(|val| Some(val.clone())) + .unwrap_or(String::new()); + + let row_type = match schema_object.get("type") { + Some(Value::String(type_)) if type_ == "string" => { + pattern_or_enum.clone() + } + Some(Value::String(type_)) if type_ == "number" => type_.clone(), + Some(Value::String(_)) => String::from("other"), + Some(_) | None => String::new(), + }; + + let row_pattern = match schema_object.get("type") { + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "pattern" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| Some(val.clone().to_string())) + .unwrap_or(String::new()) + .replace("\"", "") + } + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "enum" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| { + if let Value::Array(v) = val { + return format!( + "[{}]", + v.iter() + .map(|v| v.to_string()) + .collect::<Vec<String>>() + .join(",") + ) + .into(); + } + None + }) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) if type_ == "number" => String::new(), + Some(Value::String(_)) => schema, + _ => String::new(), + }; + + let edit_click_handler = move |_| { + let row_data = RowData { + key: row_key.clone(), + value: row_value.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + }; + + logging::log!("{:?}", row_data); + + selected_config.set(Some(row_data)); + show_modal("default_config_modal_form"); + }; + + let edit_icon: HtmlElement<html::I> = + view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; + + view! { <span on:click=edit_click_handler>{edit_icon}</span> }.into_view() + }; vec![ Column::default("key".to_string()), Column::default("schema".to_string()), Column::default("value".to_string()), Column::default("created_at".to_string()), Column::default("created_by".to_string()), - Column::new("EDIT".to_string(), None, custom_formatter), + Column::new("EDIT".to_string(), None, edit_col_formatter), ] }); view! { <div class="p-8"> - <ModalComponent handle_submit=Rc::new(move || default_config_resource.refetch())/> + <Modal + id="default_config_modal_form".to_string() + handle_close=move || { + close_modal("default_config_modal_form"); + selected_config.set(None); + } + > + + {move || { + if let Some(selected_config_data) = selected_config.get() { + view! { + <DefaultConfigForm + edit=true + config_key=selected_config_data.key + config_value=selected_config_data.value + config_type=selected_config_data.type_ + config_pattern=selected_config_data.pattern + handle_submit=move || { + default_config_resource.refetch(); + close_modal("default_config_modal_form"); + selected_config.set(None); + } + /> + } + } else { + view! { + <DefaultConfigForm handle_submit=move || { + default_config_resource.refetch(); + close_modal("default_config_modal_form"); + }/> + } + } + }} + </Modal> <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - { - let edit_signal = edit_signal.clone(); move || { let default_config = default_config_resource.get().unwrap_or(vec![]); let total_default_config_keys = default_config.len().to_string(); - let edit_signal = edit_signal.clone(); let table_rows = default_config .into_iter() .map(|config| { @@ -396,10 +198,8 @@ pub fn DefaultConfig() -> impl IntoView { <Button text="Create Key".to_string() on_click={ - let edit_signal_clone = edit_signal.to_owned(); move |_| { - edit_signal_clone.set(false); - modal_action("my_modal_5", "open"); + show_modal("default_config_modal_form") } } /> diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 2636d7d14..a0a356fce 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -71,12 +71,13 @@ pub fn Dimensions() -> impl IntoView { let row_pattern = match schema_object.get("type") { Some(Value::String(type_)) - if type_ == "string" && pattern_or_enum == "string" => + if type_ == "string" && pattern_or_enum == "pattern" => { schema_object .get(&pattern_or_enum) .and_then(|val| Some(val.clone().to_string())) .unwrap_or(String::new()) + .replace("\"", "") } Some(Value::String(type_)) if type_ == "string" && pattern_or_enum == "enum" => @@ -100,7 +101,7 @@ pub fn Dimensions() -> impl IntoView { } Some(Value::String(type_)) if type_ == "number" => String::new(), Some(Value::String(_)) => schema, - Some(_) | None => String::new(), + _ => String::new(), }; let edit_click_handler = move |_| { From 46ca970000ec435d7ac5c25200086eb5e7ac84f6 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 17 Jan 2024 20:24:30 +0530 Subject: [PATCH 236/352] fix: error resolving pages with internal call to server --- crates/context-aware-config/src/main.rs | 2 +- .../src/pages/Dimensions/Dimensions.rs | 2 +- .../frontend/src/pages/Dimensions/helper.rs | 13 ++-- .../service-utils/src/middlewares/tenant.rs | 71 +++++++++++++------ 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 7fc1a3800..da607ad1f 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -199,7 +199,7 @@ async fn main() -> Result<()> { ), ) /***************************** UI Routes ******************************/ - // .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) // serve other assets from the `assets` directory diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index a0a356fce..07134f194 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -29,7 +29,7 @@ pub fn Dimensions() -> impl IntoView { let dimensions = create_blocking_resource( move || tenant_rs.get(), |current_tenant| async move { - match fetch_dimensions(¤t_tenant).await { + match fetch_dimensions(current_tenant).await { Ok(data) => data, Err(_) => vec![], } diff --git a/crates/frontend/src/pages/Dimensions/helper.rs b/crates/frontend/src/pages/Dimensions/helper.rs index fd596d02c..1f955870d 100644 --- a/crates/frontend/src/pages/Dimensions/helper.rs +++ b/crates/frontend/src/pages/Dimensions/helper.rs @@ -1,23 +1,24 @@ use std::vec::Vec; -use crate::utils::get_host; +use leptos::{server, ServerFnError}; use super::types::Dimension; -pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { +#[server(GetDimensions, "/fxn", "GetJson")] +pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFnError> { let client = reqwest::Client::new(); - let host = get_host(); + let host = "http://localhost:8080"; let url = format!("{}/dimension", host); let response: Vec<Dimension> = client .get(url) - .header("x-tenant", tenant) + .header("x-tenant", &tenant) .send() .await - .map_err(|e| e.to_string())? + .map_err(|e| ServerFnError::ServerError(e.to_string()))? .json() .await - .map_err(|e| e.to_string())?; + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; Ok(response) } diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index b57254b51..efdd3a191 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -4,11 +4,12 @@ use crate::service::types::{AppState, Tenant}; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, error, - http::header::HeaderValue, + http::header::{HeaderMap, HeaderValue}, web::Data, Error, HttpMessage, }; use futures_util::future::LocalBoxFuture; +use log::debug; use std::rc::Rc; pub struct TenantMiddlewareFactory; @@ -35,6 +36,36 @@ pub struct TenantMiddleware<S> { service: Rc<S>, } +fn extract_tenant_from_header(headers: &HeaderMap) -> Option<String> { + headers + .get("x-tenant") + .map(|header_value: &HeaderValue| header_value.to_str().ok()) + .flatten() + .map(|header_str: &str| header_str.to_string()) +} + +fn extract_tenant_from_url(path: &str, match_pattern: Option<String>) -> Option<String> { + match_pattern + .map(move |pattern| { + let pattern_segments = pattern.split("/"); + let path_segments = path.split("/").collect::<Vec<&str>>(); + pattern_segments + .enumerate() + .find(|(_, segment)| segment == &"{tenant}") + .map(|(idx, _)| path_segments[idx].to_string()) + }) + .flatten() +} + +fn extract_tenant_from_query_params(query_str: &str) -> Option<String> { + query_str + .split("&") + .find(|segment| segment.contains("tenant=")) + .map(|tenant_query_param| tenant_query_param.split("=").nth(1)) + .flatten() + .map(|tenant_str| tenant_str.to_string()) +} + impl<S, B> Service<ServiceRequest> for TenantMiddleware<S> where S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, @@ -59,34 +90,28 @@ where } }; - let path = req.path(); - let tenant_from_params = match req.match_pattern() { - Some(pattern) => { - let pattern_segments = pattern.split("/"); - let path_segments = path.split("/").collect::<Vec<&str>>(); - Some( - pattern_segments - .enumerate() - .find(|(_, segment)| segment == &"{tenant}") - .map_or("", |(idx, _)| path_segments[idx]), - ) - } - None => None, - }; - let request_path = req.uri().path(); let is_excluded: bool = app_state .tenant_middleware_exclusion_list .contains(request_path); if !is_excluded && app_state.enable_tenant_and_scope { - let tenant = req - .headers() - .get("x-tenant") - .map_or(tenant_from_params, |header_value: &HeaderValue| { - header_value.to_str().ok() - }) - .map(|header_str| header_str.to_string()); + debug!( + "TENANT FROM HEADER ==> {:?}", + extract_tenant_from_header(req.headers()) + ); + debug!( + "TENANT FROM URL ==> {:?}", + extract_tenant_from_url(req.path(), req.match_pattern()) + ); + debug!( + "TENANT FROM QUERY ==> {:?}", + extract_tenant_from_query_params(req.query_string()) + ); + + let tenant = extract_tenant_from_header(req.headers()) + .or_else(|| extract_tenant_from_url(req.path(), req.match_pattern())) + .or_else(|| extract_tenant_from_query_params(req.query_string())); let validated_tenant: Tenant = match tenant { Some(val) if app_state.tenants.contains(&val) => Tenant(val), From cf9ab8818c1b007aaeba3563a2f14431e4727bd7 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 18 Jan 2024 08:07:02 +0000 Subject: [PATCH 237/352] chore(version): v0.17.2 [skip ci] --- CHANGELOG.md | 14 ++++++++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b074053b9..09d043e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.17.2 - 2024-01-18 +### Package updates +- service-utils bumped to service-utils-v0.10.1 +- context-aware-config bumped to context-aware-config-v0.14.2 +### Global changes +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev +- refactored DefaultConfig component + fixed edit flow - (f2d38cc) - Shubhranshu Sanjeev +- fixed dimension form edit flow + fixed table component CellFormatter to accept move closures - (9c3a364) - Shubhranshu Sanjeev +#### Refactoring +- using snake case for component fxn names - (19e9aca) - Shubhranshu Sanjeev + +- - - + ## v0.17.1 - 2024-01-12 ### Package updates - context-aware-config bumped to context-aware-config-v0.14.1 diff --git a/Cargo.lock b/Cargo.lock index 50676f6e1..71e22dbe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.14.1" +version = "0.14.2" dependencies = [ "actix", "actix-cors", @@ -3390,7 +3390,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.10.0" +version = "0.10.1" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 8a5437f85..f51115705 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.14.2 - 2024-01-18 +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.14.1 - 2024-01-12 #### Bug Fixes - frontend build process - (cbdad01) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 64c99d2a3..ff3a8d409 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.14.1" +version = "0.14.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index db48c4c7d..6915998dd 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.10.1 - 2024-01-18 +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.10.0 - 2024-01-04 #### Bug Fixes - frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index bba2d1ca2..6f326f6eb 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.10.0" +version = "0.10.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 5c0ebc33aa2f4a7ce7d2170279d6cd38cde59760 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 18 Jan 2024 16:48:02 +0530 Subject: [PATCH 238/352] fix: fixed host resolve issue for internal calls in SSR. --- crates/frontend/src/api.rs | 150 ++++++++++-------- .../components/context_form/context_form.rs | 2 +- .../experiment_form/experiment_form.rs | 4 +- .../src/components/experiment_form/types.rs | 2 +- .../src/components/experiment_form/utils.rs | 2 +- .../components/override_form/override_form.rs | 2 +- .../pages/ContextOverride/ContextOverride.rs | 8 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 4 +- .../pages/ExperimentList/ExperimentList.rs | 17 +- .../frontend/src/pages/ExperimentList/mod.rs | 1 - .../src/pages/ExperimentList/types.rs | 84 ---------- .../src/pages/ExperimentList/utils.rs | 77 --------- crates/frontend/src/pages/Home/Home.rs | 7 +- crates/frontend/src/pages/mod.rs | 20 --- crates/frontend/src/pages/types.rs | 16 -- crates/frontend/src/types.rs | 102 ++++++++++++ .../service-utils/src/middlewares/tenant.rs | 23 +-- 17 files changed, 224 insertions(+), 297 deletions(-) delete mode 100644 crates/frontend/src/pages/ExperimentList/types.rs delete mode 100644 crates/frontend/src/pages/types.rs diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 56e0a359b..bb1cb3ca7 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,76 +1,100 @@ -use crate::{ - pages::ExperimentList::types::{DefaultConfig, Dimension}, - utils::get_host, -}; +use leptos::{server, ServerFnError}; -// #[derive(Debug, Serialize, Deserialize, Clone)] -// pub struct Dimension { -// pub dimension: String, -// pub priority: i32, -// pub created_at: DateTime<Utc>, -// pub created_by: String, -// pub schema: Value, -// } +use crate::types::{Config, DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; -// #[derive(Serialize, Deserialize, Clone, Debug)] -// pub struct DefaultConfig { -// pub key: String, -// pub value: Value, -// pub created_at: DateTime<Utc>, -// pub created_by: String, -// pub schema: Value, -// } +#[server(GetDimensions, "/fxn", "GetJson")] +pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFnError> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + + let url = format!("{}/dimension", host); + let response: Vec<Dimension> = client + .get(url) + .header("x-tenant", &tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} -pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, String> { +#[server(GetDefaultConfig, "/fxn", "GetJson")] +pub async fn fetch_default_config( + tenant: String, +) -> Result<Vec<DefaultConfig>, ServerFnError> { let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/dimension"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let dimensions = response.json().await.map_err(|e| e.to_string())?; - Ok(dimensions) - } - Err(e) => Err(e.to_string()), + let host = "http://localhost:8080"; + + let url = format!("{}/default-config", host); + let response: Vec<DefaultConfig> = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +#[server(GetExperiments, "/fxn", "GetJson")] +pub async fn fetch_experiments( + filters: ListFilters, + tenant: String, +) -> Result<ExperimentsResponse, ServerFnError> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + + let mut query_params = vec![]; + if let Some(status) = filters.status { + let status: Vec<String> = status.iter().map(|val| val.to_string()).collect(); + query_params.push(format!("status={}", status.join(","))); + } + if let Some(from_date) = filters.from_date { + query_params.push(format!("from_date={}", from_date)); + } + if let Some(to_date) = filters.to_date { + query_params.push(format!("to_date={}", to_date)); + } + if let Some(page) = filters.page { + query_params.push(format!("page={}", page)); } + if let Some(count) = filters.count { + query_params.push(format!("count={}", count)); + } + + let url = format!("{}/experiments?{}", host, query_params.join("&")); + let response: ExperimentsResponse = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) } -pub async fn fetch_default_config(tenant: String) -> Result<Vec<DefaultConfig>, String> { +#[server(GetConfig, "/fxn", "GetJson")] +pub async fn fetch_config(tenant: String) -> Result<Config, ServerFnError> { let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/default-config"); + let host = "http://localhost:8080"; + let url = format!("{host}/config"); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { - let default_config = response.json().await.map_err(|e| e.to_string())?; - Ok(default_config) + let config: Config = response + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + Ok(config) } - Err(e) => Err(e.to_string()), + Err(e) => Err(ServerFnError::ServerError(e.to_string())), } } - -//pub fn dimension_resource( -// tenant: ReadSignal<String>, -//) -> Resource<String, Vec<Dimension>> { -// create_blocking_resource( -// move || tenant.get(), -// |tenant| async { -// match fetch_dimensions(tenant).await { -// Ok(data) => data, -// Err(_) => vec![], -// } -// }, -// ) -//} - -//pub fn default_config_resource( -// tenant: ReadSignal<String>, -//) -> Resource<String, Vec<DefaultConfig>> { -// create_blocking_resource( -// move || tenant.get(), -// |tenant| async { -// match fetch_default_config(tenant).await { -// Ok(data) => data, -// Err(_) => vec![], -// } -// }, -//) -//} diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index a5ad3da79..f5299ebde 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -1,4 +1,4 @@ -use crate::pages::ExperimentList::types::Dimension; +use crate::types::Dimension; use leptos::*; use std::cmp; use std::collections::HashSet; diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 4a48c06c9..2365d1de5 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -3,9 +3,7 @@ use crate::components::button::button::Button; use crate::components::{ context_form::context_form::ContextForm, override_form::override_form::OverrideForm, }; -use crate::pages::ExperimentList::types::{ - DefaultConfig, Dimension, Variant, VariantType, -}; +use crate::types::{DefaultConfig, Dimension, Variant, VariantType}; use chrono::offset::Local; use leptos::*; use serde::{Deserialize, Serialize}; diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs index 9bb2ee0f9..a84f13183 100644 --- a/crates/frontend/src/components/experiment_form/types.rs +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -1,4 +1,4 @@ -use crate::pages::ExperimentList::types::Variant; +use crate::types::Variant; use serde::Serialize; use serde_json::Value; diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index a89f0106d..a9f06d3b8 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -1,6 +1,6 @@ use super::types::ExperimentCreateRequest; use crate::components::context_form::utils::construct_context; -use crate::pages::ExperimentList::types::Variant; +use crate::types::Variant; use crate::utils::get_host; use reqwest::StatusCode; use serde_json::json; diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 604942c5c..8897826ba 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,4 +1,4 @@ -use crate::pages::ExperimentList::types::DefaultConfig; +use crate::types::DefaultConfig; use leptos::*; use serde_json::{json, Map, Value}; use std::collections::HashSet; diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 5f954afe7..2b8bffede 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -1,12 +1,12 @@ use std::rc::Rc; +use crate::api::fetch_config; use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::button::button::Button; use crate::components::condition_pills::condition_pills::ContextPills; use crate::components::context_form::context_form::ContextForm; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; -use crate::pages::fetch_config; use crate::utils::{get_host, modal_action}; use leptos::*; use reqwest::StatusCode; @@ -53,7 +53,7 @@ where Some(Err(error)) => { view! { <div class="text-red-500"> - {"Failed to fetch config data: "} {error} + {"Failed to fetch config data: "} {error.to_string()} </div> } } @@ -176,7 +176,7 @@ where Some(Err(error)) => { view! { <div class="text-red-500"> - {"Failed to fetch config data: "} {error} + {"Failed to fetch config data: "} {error.to_string()} </div> } } @@ -384,7 +384,7 @@ pub fn ContextOverride() -> impl IntoView { vec![ view! { <div class="text-red-500"> - {"Failed to fetch config data: "} {error} + {"Failed to fetch config data: "} {error.to_string()} </div> }, ] diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 751bf227e..0ade94ce5 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -4,9 +4,9 @@ use crate::components::default_config_form::default_config_form::DefaultConfigFo use crate::components::modal::modal::Modal; use crate::components::table::{table::Table, types::Column}; +use crate::api::fetch_default_config; use crate::components::button::button::Button; use crate::components::stat::stat::Stat; -use crate::pages::ExperimentList::utils::fetch_default_config; use crate::utils::{close_modal, show_modal}; use leptos::*; use serde_json::{json, Map, Value}; @@ -25,7 +25,7 @@ pub fn DefaultConfig() -> impl IntoView { let default_config_resource = create_blocking_resource( move || tenant_rs.get(), |current_tenant| async move { - match fetch_default_config(¤t_tenant).await { + match fetch_default_config(current_tenant).await { Ok(data) => data, Err(_) => vec![], } diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs index e68000b83..6cd6fb63a 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/ExperimentList/ExperimentList.rs @@ -10,14 +10,12 @@ use crate::components::{ pagination::pagination::Pagination, stat::stat::Stat, table::table::Table, }; -use crate::pages::ExperimentList::types::{ExperimentsResponse, ListFilters}; +use crate::types::{ExperimentsResponse, ListFilters}; -use super::{ +use super::utils::experiment_table_columns; +use crate::{ + api::{fetch_default_config, fetch_dimensions, fetch_experiments}, types::{DefaultConfig, Dimension}, - utils::{ - experiment_table_columns, fetch_default_config, fetch_dimensions, - fetch_experiments, - }, }; use serde_json::{json, Map, Value}; use wasm_bindgen::JsCast; @@ -49,9 +47,10 @@ pub fn ExperimentList() -> impl IntoView { move || (tenant_rs.get(), filters.get()), |(current_tenant, filters)| async move { // Perform all fetch operations concurrently - let experiments_future = fetch_experiments(filters, ¤t_tenant); - let dimensions_future = fetch_dimensions(¤t_tenant); - let config_future = fetch_default_config(¤t_tenant); + let experiments_future = + fetch_experiments(filters, current_tenant.to_string()); + let dimensions_future = fetch_dimensions(current_tenant.to_string()); + let config_future = fetch_default_config(current_tenant.to_string()); let (experiments_result, dimensions_result, config_result) = join!(experiments_future, dimensions_future, config_future); diff --git a/crates/frontend/src/pages/ExperimentList/mod.rs b/crates/frontend/src/pages/ExperimentList/mod.rs index 298691e82..bbb7196fb 100644 --- a/crates/frontend/src/pages/ExperimentList/mod.rs +++ b/crates/frontend/src/pages/ExperimentList/mod.rs @@ -1,3 +1,2 @@ pub mod ExperimentList; -pub mod types; pub mod utils; diff --git a/crates/frontend/src/pages/ExperimentList/types.rs b/crates/frontend/src/pages/ExperimentList/types.rs deleted file mode 100644 index fc0b6ce4c..000000000 --- a/crates/frontend/src/pages/ExperimentList/types.rs +++ /dev/null @@ -1,84 +0,0 @@ -use chrono::{DateTime, Utc}; -use derive_more::{Deref, DerefMut}; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; - -#[derive( - Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, -)] -#[strum(serialize_all = "UPPERCASE")] -pub enum ExperimentStatusType { - CREATED, - CONCLUDED, - INPROGRESS, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ExperimentResponse { - pub id: String, - pub created_at: DateTime<Utc>, - pub created_by: String, - pub last_modified: DateTime<Utc>, - - pub name: String, - pub override_keys: Vec<String>, - pub status: ExperimentStatusType, - pub traffic_percentage: i32, - - pub context: Value, - pub variants: Value, - pub chosen_variant: Option<String>, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ExperimentsResponse { - pub total_items: i64, - pub total_pages: i64, - pub data: Vec<ExperimentResponse>, -} - -#[derive(Deserialize, Debug, Clone, Deref, DerefMut, PartialEq)] -pub struct StatusTypes(pub Vec<ExperimentStatusType>); - -#[derive(Debug, Clone, PartialEq)] -pub struct ListFilters { - pub status: Option<StatusTypes>, - pub from_date: Option<DateTime<Utc>>, - pub to_date: Option<DateTime<Utc>>, - pub page: Option<i64>, - pub count: Option<i64>, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Dimension { - pub dimension: String, - pub priority: i32, - pub created_at: DateTime<Utc>, - pub created_by: String, - pub schema: Value, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct DefaultConfig { - pub key: String, - pub value: Value, - pub created_at: DateTime<Utc>, - pub created_by: String, - pub schema: Value, -} - -#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, strum_macros::Display)] -#[strum(serialize_all = "UPPERCASE")] -pub enum VariantType { - CONTROL, - EXPERIMENTAL, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct Variant { - pub id: String, - pub variant_type: VariantType, - pub context_id: Option<String>, - pub override_id: Option<String>, - pub overrides: Map<String, Value>, -} diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 2b526f0f6..79c0b20ad 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -1,8 +1,6 @@ -use super::types::{DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; use crate::components::{ condition_pills::condition_pills::ContextPills, table::types::Column, }; -use crate::utils::get_host; use core::time::Duration; use leptos::*; use leptos_router::A; @@ -10,81 +8,6 @@ use serde_json::{json, Map, Value}; use std::vec::Vec; use web_sys::MouseEvent; -pub async fn fetch_experiments( - filters: ListFilters, - tenant: &String, -) -> Result<ExperimentsResponse, String> { - let client = reqwest::Client::new(); - let host = get_host(); - - let mut query_params = vec![]; - if let Some(status) = filters.status { - let status: Vec<String> = status.iter().map(|val| val.to_string()).collect(); - query_params.push(format!("status={}", status.join(","))); - } - if let Some(from_date) = filters.from_date { - query_params.push(format!("from_date={}", from_date)); - } - if let Some(to_date) = filters.to_date { - query_params.push(format!("to_date={}", to_date)); - } - if let Some(page) = filters.page { - query_params.push(format!("page={}", page)); - } - if let Some(count) = filters.count { - query_params.push(format!("count={}", count)); - } - - let url = format!("{}/experiments?{}", host, query_params.join("&")); - let response: ExperimentsResponse = client - .get(url) - .header("x-tenant", tenant) - .send() - .await - .map_err(|e| e.to_string())? - .json() - .await - .map_err(|e| e.to_string())?; - - Ok(response) -} - -pub async fn fetch_dimensions(tenant: &str) -> Result<Vec<Dimension>, String> { - let client = reqwest::Client::new(); - let host = get_host(); - - let url = format!("{}/dimension", host); - let response: Vec<Dimension> = client - .get(url) - .header("x-tenant", tenant) - .send() - .await - .map_err(|e| e.to_string())? - .json() - .await - .map_err(|e| e.to_string())?; - - Ok(response) -} - -pub async fn fetch_default_config(tenant: &str) -> Result<Vec<DefaultConfig>, String> { - let client = reqwest::Client::new(); - let host = get_host(); - - let url = format!("{}/default-config", host); - let response: Vec<DefaultConfig> = client - .get(url) - .header("x-tenant", tenant) - .send() - .await - .map_err(|e| e.to_string())? - .json() - .await - .map_err(|e| e.to_string())?; - - Ok(response) -} - pub fn experiment_table_columns() -> Vec<Column> { vec![ Column::new( diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index c273e8e40..1adb2fa0f 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -4,13 +4,12 @@ use wasm_bindgen::JsCast; use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; use crate::{ - api::fetch_dimensions, + api::{fetch_config, fetch_dimensions}, components::{ button::button::Button, condition_pills::utils::{extract_and_format, parse_conditions}, context_form::context_form::ContextForm, }, - pages::fetch_config, utils::get_host, }; @@ -307,7 +306,7 @@ pub fn home() -> impl IntoView { Some(Err(error)) => { view! { <div class="error"> - {"Failed to fetch config data: "} {error} + {"Failed to fetch config data: "} {error.to_string()} </div> } } @@ -448,7 +447,7 @@ pub fn home() -> impl IntoView { vec![ view! { <div class="error"> - {"Failed to fetch config data: "} {error} + {"Failed to fetch config data: "} {error.to_string()} </div> }, ] diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 2ed892a86..3e14dd908 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,9 +1,5 @@ #![allow(non_snake_case)] -use crate::utils::get_host; - -use self::types::Config; - pub mod ContextOverride; pub mod DefaultConfig; pub mod Dimensions; @@ -11,19 +7,3 @@ pub mod Experiment; pub mod ExperimentList; pub mod Home; pub mod NotFound; -pub mod types; - -// Utils segments found here - -pub async fn fetch_config(tenant: String) -> Result<Config, String> { - let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/config"); - match client.get(url).header("x-tenant", tenant).send().await { - Ok(response) => { - let config: Config = response.json().await.map_err(|e| e.to_string())?; - Ok(config) - } - Err(e) => Err(e.to_string()), - } -} diff --git a/crates/frontend/src/pages/types.rs b/crates/frontend/src/pages/types.rs deleted file mode 100644 index adca618bf..000000000 --- a/crates/frontend/src/pages/types.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; - -#[derive(Deserialize, Serialize, Clone)] -pub struct Context { - pub id: String, - pub condition: Value, - pub override_with_keys: [String; 1], -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Config { - pub contexts: Vec<Context>, - pub overrides: Map<String, Value>, - pub default_configs: Map<String, Value>, -} diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index 2f9b4288b..700aff9bf 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -2,6 +2,10 @@ use leptos::{ReadSignal, WriteSignal}; use serde::{Deserialize, Serialize}; use std::{str::FromStr, vec::Vec}; +use chrono::{DateTime, Utc}; +use derive_more::{Deref, DerefMut}; +use serde_json::{Map, Value}; + #[derive(Clone, Debug)] pub struct AppRoute { pub key: String, @@ -38,3 +42,101 @@ pub struct Envs { pub app_env: AppEnv, pub tenants: Vec<String>, } + +/*********************** Experimentation Types ****************************************/ + +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] +#[strum(serialize_all = "UPPERCASE")] +pub enum ExperimentStatusType { + CREATED, + CONCLUDED, + INPROGRESS, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub last_modified: DateTime<Utc>, + + pub name: String, + pub override_keys: Vec<String>, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, + pub chosen_variant: Option<String>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExperimentsResponse { + pub total_items: i64, + pub total_pages: i64, + pub data: Vec<ExperimentResponse>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, PartialEq)] +pub struct StatusTypes(pub Vec<ExperimentStatusType>); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ListFilters { + pub status: Option<StatusTypes>, + pub from_date: Option<DateTime<Utc>>, + pub to_date: Option<DateTime<Utc>>, + pub page: Option<i64>, + pub count: Option<i64>, +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +/*************************** Context-Override types ********************************/ + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DefaultConfig { + pub key: String, + pub value: Value, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option<String>, + pub override_id: Option<String>, + pub overrides: Map<String, Value>, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index efdd3a191..b6ad70035 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -36,34 +36,35 @@ pub struct TenantMiddleware<S> { service: Rc<S>, } -fn extract_tenant_from_header(headers: &HeaderMap) -> Option<String> { +fn extract_tenant_from_header(headers: &HeaderMap) -> Option<&str> { headers .get("x-tenant") .map(|header_value: &HeaderValue| header_value.to_str().ok()) .flatten() - .map(|header_str: &str| header_str.to_string()) } -fn extract_tenant_from_url(path: &str, match_pattern: Option<String>) -> Option<String> { +fn extract_tenant_from_url<'a>( + path: &'a str, + match_pattern: Option<String>, +) -> Option<&'a str> { match_pattern .map(move |pattern| { let pattern_segments = pattern.split("/"); let path_segments = path.split("/").collect::<Vec<&str>>(); - pattern_segments - .enumerate() - .find(|(_, segment)| segment == &"{tenant}") - .map(|(idx, _)| path_segments[idx].to_string()) + + std::iter::zip(path_segments, pattern_segments) + .find(|(_, pattern_seg)| pattern_seg == &"{tenant}") + .map(|(path_seg, _)| path_seg) }) .flatten() } -fn extract_tenant_from_query_params(query_str: &str) -> Option<String> { +fn extract_tenant_from_query_params(query_str: &str) -> Option<&str> { query_str .split("&") .find(|segment| segment.contains("tenant=")) .map(|tenant_query_param| tenant_query_param.split("=").nth(1)) .flatten() - .map(|tenant_str| tenant_str.to_string()) } impl<S, B> Service<ServiceRequest> for TenantMiddleware<S> @@ -114,7 +115,9 @@ where .or_else(|| extract_tenant_from_query_params(req.query_string())); let validated_tenant: Tenant = match tenant { - Some(val) if app_state.tenants.contains(&val) => Tenant(val), + Some(val) if app_state.tenants.contains(val) => { + Tenant(String::from(val)) + } Some(_) => { return Err(error::ErrorBadRequest("invalid x-tenant value")); } From c0f69aa8c761f5c03885eba94eb88681807dd2cc Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 22 Jan 2024 09:21:52 +0000 Subject: [PATCH 239/352] chore(version): v0.17.3 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d043e16..aacba287c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.17.3 - 2024-01-22 +### Package updates +- service-utils bumped to service-utils-v0.10.2 +### Global changes +#### Bug Fixes +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev + +- - - + ## v0.17.2 - 2024-01-18 ### Package updates - service-utils bumped to service-utils-v0.10.1 diff --git a/Cargo.lock b/Cargo.lock index 71e22dbe7..0e9b95f12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3390,7 +3390,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.10.1" +version = "0.10.2" dependencies = [ "actix", "actix-web", diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 6915998dd..364739e4b 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.10.2 - 2024-01-22 +#### Bug Fixes +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.10.1 - 2024-01-18 #### Bug Fixes - error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 6f326f6eb..f46fb7201 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.10.1" +version = "0.10.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 917aa7317843c959d6abd325159111506e4bdcb8 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 18 Jan 2024 12:25:24 +0530 Subject: [PATCH 240/352] fix: added partitions for 2025 and 2026 for audit table --- .../down.sql | 1 + .../up.sql | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql create mode 100644 crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql diff --git a/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql b/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql b/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql new file mode 100644 index 000000000..dbd5c77b2 --- /dev/null +++ b/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql @@ -0,0 +1,118 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS public.event_log_y2024m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-08-01') TO ('2024-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-09-01') TO ('2024-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-10-01') TO ('2024-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-11-01') TO ('2024-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-12-01') TO ('2025-01-01'); + +------------ Parititions for 2025 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2025m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-01-01') TO ('2025-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-02-01') TO ('2025-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-03-01') TO ('2025-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-04-01') TO ('2025-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-05-01') TO ('2025-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-06-01') TO ('2025-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-07-01') TO ('2025-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-08-01') TO ('2025-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-09-01') TO ('2025-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-10-01') TO ('2025-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-11-01') TO ('2025-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-12-01') TO ('2026-01-01'); + +------------ Parititions for 2026 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2026m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-01-01') TO ('2026-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-02-01') TO ('2026-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-03-01') TO ('2026-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-04-01') TO ('2026-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-05-01') TO ('2026-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-06-01') TO ('2026-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-07-01') TO ('2026-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-08-01') TO ('2026-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-09-01') TO ('2026-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-10-01') TO ('2026-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-11-01') TO ('2026-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-12-01') TO ('2027-01-01'); \ No newline at end of file From cebd182aac5734e907bfff4527e1aa022ecf78b1 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 22 Jan 2024 13:15:03 +0000 Subject: [PATCH 241/352] chore(version): v0.17.4 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aacba287c..b9f08ca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.17.4 - 2024-01-22 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.1 +### Global changes + +- - - + ## v0.17.3 - 2024-01-22 ### Package updates - service-utils bumped to service-utils-v0.10.2 diff --git a/Cargo.lock b/Cargo.lock index 0e9b95f12..c89d109f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.9.0" +version = "0.9.1" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 4a501677b..e3a4172e6 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.9.1 - 2024-01-22 +#### Bug Fixes +- added partitions for 2025 and 2026 for audit table - (45d37dd) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.9.0 - 2024-01-04 #### Bug Fixes - fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 725328eac..755d47ab8 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.9.0" +version = "0.9.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8141696107006a0bd42980e7ad6a34aff7a6f3ed Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Mon, 22 Jan 2024 18:30:10 +0530 Subject: [PATCH 242/352] fix: getting api hostname from env for frontend --- .env.example | 1 + crates/frontend/src/app.rs | 13 +++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index b1e95bc53..2a99f50d9 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,7 @@ ALLOW_SAME_KEYS_OVERLAPPING_CTX=true ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true CAC_HOST="http://localhost:8080" +API_HOSTNAME="http://localhost:8080" CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" HOSTNAME="<application_name>-<deployment_id>-<replicaset>-<pod>" MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 9b186817c..e78ba30e2 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -36,18 +36,11 @@ async fn load_envs() -> Envs { .map(|tenant| tenant.to_string()) .collect::<Vec<String>>(); - logging::log!("{:?}", tenants); - - let host = match app_env { - AppEnv::PROD => { - "https://context-aware-config.sso.internal.svc.k8s.apoc.mum.juspay.net" - } - AppEnv::SANDBOX => "https://context-aware.internal.staging.mum.juspay.net", - AppEnv::DEV => "http://localhost:8080", - }; + let host = get_from_env_unsafe::<String>("API_HOSTNAME") + .unwrap_or(String::from("http://localhost:8080")); Envs { - host: host.to_string(), + host, app_env, tenants, } From 4bf9ac822808269af80ef17c0b0bb469f098843e Mon Sep 17 00:00:00 2001 From: Akhilesh Bhadauriya <akhilesh.b@juspay.in> Date: Fri, 5 Jan 2024 17:31:10 +0530 Subject: [PATCH 243/352] feat: added between in frontend --- .../condition_pills/condition_pills.rs | 35 +++++++++++++++- .../src/components/condition_pills/utils.rs | 24 ++++++++++- .../components/context_form/context_form.rs | 1 + .../pages/ContextOverride/ContextOverride.rs | 42 ++++++++++++------- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs index 16a692fe4..0f5391f4b 100644 --- a/crates/frontend/src/components/condition_pills/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -11,6 +11,7 @@ pub fn context_pills(context: Value) -> impl IntoView { {ctx_values .into_iter() .map(|(dim, op, val)| { + let operator = op.clone(); view! { <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> <span class="font-mono font-medium context_condition text-gray-500"> @@ -19,7 +20,39 @@ pub fn context_pills(context: Value) -> impl IntoView { <span class="font-mono font-medium text-gray-650 context_condition "> {op} </span> - <span class="font-mono font-semibold context_condition">{val}</span> + + {match operator.trim() { + "BETWEEN" => { + let split_val: Vec<String> = val + .clone() + .split(",") + .map(String::from) + .collect(); + view! { + <> + <span class="font-mono font-semibold context_condition"> + {&split_val[0]} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {"and"} + </span> + <span class="font-mono font-semibold context_condition"> + {&split_val[1]} + </span> + </> + } + } + _ => { + view! { + <> + <span class="font-mono font-semibold context_condition"> + {val} + </span> + </> + } + } + }} + </span> } }) diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs index f4ca37da5..95b75d3b5 100644 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -2,7 +2,7 @@ use serde_json::Value; pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { let mut conditions = Vec::new(); - let operators = vec!["==", "in"]; + let operators = vec!["==", "in", "<="]; // Split the string by "&&" and iterate over each condition for condition in input.split("&&") { @@ -33,6 +33,9 @@ pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { if op == "==".to_string() { val = val.trim_matches('"').to_string(); op = "is".to_string(); + } else if op == "<=".to_string() { + val = val.trim_matches('"').to_string(); + op = "BETWEEN".to_string(); } else { val = val.trim_matches('"').to_string(); op = "has".to_string(); @@ -89,6 +92,25 @@ fn format_condition(condition: &Value) -> String { } } + // Handline the "<=" operator differently + if operator.as_str() == "<=" { + let left_operand = &operands[0]; + let right_operand = &operands[2]; + let mid_operand = &operands[1]; + + let left_str = format!("{}", left_operand).trim_matches('"').to_string(); + let right_str = format!("{}", right_operand).trim_matches('"').to_string(); + + if mid_operand.is_object() && mid_operand["var"].is_string() { + let var_str = mid_operand["var"].as_str().unwrap(); + return format!( + "{} {} {}", + var_str, + operator, + left_str + "," + &right_str + ); + } + } // Handling regular operators if let Some(first_operand) = operands.get(0) { if first_operand.is_object() && first_operand["var"].is_string() { diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index f5299ebde..42971fb61 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -143,6 +143,7 @@ where </option> <option value="==">"IS"</option> <option value="IN">"HAS"</option> + <option value="<=">"BETWEEN (inclusive)"</option> </select> </div> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 2b8bffede..ac8a45fbc 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -76,28 +76,42 @@ pub fn construct_request_payload( ) -> Value { // Construct the override section let override_section: Map<String, Value> = overrides; + let between_operators = |val: &str, variable: &str| { + let split_value: Vec<&str> = val.split(',').collect(); + json!({ + "<=": [ + split_value[0].trim(), + { "var": variable }, + split_value[1].trim() + ] + }) + }; - // Construct the context section - let context_section = if conditions.len() == 1 { - // Single condition - let (variable, operator, value) = &conditions[0]; + let other_operators = |op: &str, val: &str, variable: &str| { json!({ - operator: [ + op: [ { "var": variable }, - value + val ] }) + }; + + let context_section = if conditions.len() == 1 { + let (variable, operator, value) = &conditions[0]; + if operator == "<=" { + between_operators(value, variable) + } else { + other_operators(operator, value, variable) + } } else { - // Multiple conditions inside an "and" let and_conditions: Vec<Value> = conditions - .into_iter() + .iter() // Use iter() instead of into_iter() to avoid consuming conditions .map(|(variable, operator, value)| { - json!({ - operator: [ - { "var": variable }, - value - ] - }) + if operator == "<=" { + between_operators(value, variable) + } else { + other_operators(operator, value, variable) + } }) .collect(); From be4b6375155d28b91baf9cbd95fe890a863e5024 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 22 Jan 2024 13:59:58 +0000 Subject: [PATCH 244/352] chore(version): v0.18.0 [skip ci] --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f08ca3a..a534dfcbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.18.0 - 2024-01-22 +### Packages +- experimentation-platform locked to experimentation-platform-v0.9.1 +- cac_client locked to cac_client-v0.5.0 +- external locked to external-v0.3.0 +- service-utils locked to service-utils-v0.10.2 +- superposition_client locked to superposition_client-v0.4.0 +- context-aware-config locked to context-aware-config-v0.14.2 +### Global changes +#### Bug Fixes +- getting api hostname from env for frontend - (837899d) - Shubhranshu Sanjeev +#### Features +- added between in frontend - (0eb60e5) - Akhilesh Bhadauriya + +- - - + ## v0.17.4 - 2024-01-22 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.9.1 From 0ca6832ef027a8063c8b3eb1ccdca3516bd03b86 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Mon, 29 Jan 2024 17:31:09 +0530 Subject: [PATCH 245/352] ci: added NY ECR registry push to Jenkins --- Jenkinsfile | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index a1f0af955..dc7dd59a3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,6 +11,7 @@ pipeline { REGION = "ap-south-1"; REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); + REGISTRY_HOST_NY_PROD = getRegistryHost("147728078333", REGION); AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" GIT_REPO_NAME = "context-aware-config" @@ -177,6 +178,21 @@ pipeline { } } + stage('Push Image To NY Production Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_NY_PROD} + ''' + } + } + stage('Create Integ Release Tracker') { when { expression { SKIP_CI == 'false' } @@ -227,4 +243,3 @@ pipeline { } } } - From 7d9c5fe2327382a7731ed5752bfa64b76bfe8ccc Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Tue, 23 Jan 2024 18:09:08 +0530 Subject: [PATCH 246/352] fix: added partitions for audit_log table in cac schema --- .../down.sql | 1 + .../up.sql | 118 +++++ crates/context-aware-config/src/db/schema.rs | 406 ++++++++++++++++++ .../experimentation-platform/src/db/schema.rs | 406 ++++++++++++++++++ scripts/create-tenant.sh | 33 +- 5 files changed, 954 insertions(+), 10 deletions(-) create mode 100644 crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql create mode 100644 crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql diff --git a/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql b/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql b/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql new file mode 100644 index 000000000..dbd5c77b2 --- /dev/null +++ b/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql @@ -0,0 +1,118 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS public.event_log_y2024m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-08-01') TO ('2024-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-09-01') TO ('2024-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-10-01') TO ('2024-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-11-01') TO ('2024-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-12-01') TO ('2025-01-01'); + +------------ Parititions for 2025 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2025m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-01-01') TO ('2025-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-02-01') TO ('2025-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-03-01') TO ('2025-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-04-01') TO ('2025-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-05-01') TO ('2025-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-06-01') TO ('2025-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-07-01') TO ('2025-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-08-01') TO ('2025-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-09-01') TO ('2025-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-10-01') TO ('2025-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-11-01') TO ('2025-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-12-01') TO ('2026-01-01'); + +------------ Parititions for 2026 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2026m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-01-01') TO ('2026-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-02-01') TO ('2026-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-03-01') TO ('2026-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-04-01') TO ('2026-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-05-01') TO ('2026-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-06-01') TO ('2026-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-07-01') TO ('2026-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-08-01') TO ('2026-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-09-01') TO ('2026-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-10-01') TO ('2026-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-11-01') TO ('2026-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-12-01') TO ('2027-01-01'); \ No newline at end of file diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 6a60f2bf3..441e4b283 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -202,6 +202,383 @@ diesel::table! { } } +diesel::table! { + event_log_y2024m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, @@ -219,4 +596,33 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2024m05, event_log_y2024m06, event_log_y2024m07, + event_log_y2024m08, + event_log_y2024m09, + event_log_y2024m10, + event_log_y2024m11, + event_log_y2024m12, + event_log_y2025m01, + event_log_y2025m02, + event_log_y2025m03, + event_log_y2025m04, + event_log_y2025m05, + event_log_y2025m06, + event_log_y2025m07, + event_log_y2025m08, + event_log_y2025m09, + event_log_y2025m10, + event_log_y2025m11, + event_log_y2025m12, + event_log_y2026m01, + event_log_y2026m02, + event_log_y2026m03, + event_log_y2026m04, + event_log_y2026m05, + event_log_y2026m06, + event_log_y2026m07, + event_log_y2026m08, + event_log_y2026m09, + event_log_y2026m10, + event_log_y2026m11, + event_log_y2026m12, ); diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 143ddfa86..a8676f480 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -175,6 +175,383 @@ diesel::table! { } } +diesel::table! { + event_log_y2024m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable<Json>, + new_data -> Nullable<Json>, + query -> Text, + } +} + diesel::table! { use diesel::sql_types::*; use super::sql_types::ExperimentStatusType; @@ -209,5 +586,34 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2024m05, event_log_y2024m06, event_log_y2024m07, + event_log_y2024m08, + event_log_y2024m09, + event_log_y2024m10, + event_log_y2024m11, + event_log_y2024m12, + event_log_y2025m01, + event_log_y2025m02, + event_log_y2025m03, + event_log_y2025m04, + event_log_y2025m05, + event_log_y2025m06, + event_log_y2025m07, + event_log_y2025m08, + event_log_y2025m09, + event_log_y2025m10, + event_log_y2025m11, + event_log_y2025m12, + event_log_y2026m01, + event_log_y2026m02, + event_log_y2026m03, + event_log_y2026m04, + event_log_y2026m05, + event_log_y2026m06, + event_log_y2026m07, + event_log_y2026m08, + event_log_y2026m09, + event_log_y2026m10, + event_log_y2026m11, + event_log_y2026m12, experiments, ); diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index c8e0dbafc..c912e434c 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +shopt -s extglob + TENANT=$1 DB_URL=$2 @@ -9,17 +11,28 @@ echo "DB URL ==> $DB_URL" CAC_SCHEMA="${TENANT}_cac" EXP_SCHEMA="${TENANT}_experimentation" -cp -r "crates/context-aware-config/migrations/." "crates/context-aware-config/${TENANT}_migrations" -find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${CAC_SCHEMA}/g" +function generate_sql() { + service=$1 + schema=$2 + + rm ${schema}.sql + + for f in $(find "crates/$service/migrations" -name "up.sql" | grep -v "diesel_initial_setup" | sort) + do + OLDIFS=$IFS + IFS= + sql="$(cat $f | sed "s/public/${schema}/g")" + echo $sql >> "${schema}.sql" + IFS=$OLDIFS + done -cp -r "crates/experimentation-platform/migrations/." "crates/experimentation-platform/${TENANT}_migrations" -find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" | xargs sed -i'' "s/public/${EXP_SCHEMA}/g" + echo "Generated ${schema}.sql" -echo "Creating $CAC_SCHEMA" -find "crates/context-aware-config/${TENANT}_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; + echo "Running migrations for $schema" + psql "$DB_URL" -f ${schema}.sql +} -echo "Creating $EXP_SCHEMA" -find "crates/experimentation-platform/${TENANT}_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; +generate_sql "context-aware-config" $CAC_SCHEMA +generate_sql "experimentation-platform" $EXP_SCHEMA -rm -rf "crates/context-aware-config/${TENANT}_migrations" -rm -rf "crates/experimentation-platform/${TENANT}_migrations" \ No newline at end of file +shopt -u extglob \ No newline at end of file From 811bbaef0030f6a5e6ac6a33196c6a78197b91ac Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Mon, 29 Jan 2024 18:46:43 +0530 Subject: [PATCH 247/352] ci: removing test tenant sqls after ci-test --- makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/makefile b/makefile index e17ec0851..6a74d463e 100644 --- a/makefile +++ b/makefile @@ -129,6 +129,8 @@ run: kill build ci-test: cleanup ci-setup cargo test npm run test + rm test_cac.sql + rm test_experimentation.sql ci-build: docker buildx build --ssh default=$(SSH_AUTH_SOCK) \ From c52344eaaba8b9216989d98636112caeaa36735c Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 29 Jan 2024 13:29:30 +0000 Subject: [PATCH 248/352] chore(version): v0.18.1 [skip ci] --- CHANGELOG.md | 13 +++++++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a534dfcbd..56346aa55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.18.1 - 2024-01-29 +### Package updates +- context-aware-config bumped to context-aware-config-v0.14.3 +- experimentation-platform bumped to experimentation-platform-v0.9.2 +### Global changes +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev +#### Continuous Integration +- removing test tenant sqls after ci-test - (d1e42db) - Shubhranshu Sanjeev +- added NY ECR registry push to Jenkins - (51995ae) - Shubhranshu Sanjeev + +- - - + ## v0.18.0 - 2024-01-22 ### Packages - experimentation-platform locked to experimentation-platform-v0.9.1 diff --git a/Cargo.lock b/Cargo.lock index c89d109f4..ba843304f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.14.2" +version = "0.14.3" dependencies = [ "actix", "actix-cors", @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.9.1" +version = "0.9.2" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index f51115705..63cdb3531 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.14.3 - 2024-01-29 +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.14.2 - 2024-01-18 #### Bug Fixes - error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index ff3a8d409..0bb6b0f55 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.14.2" +version = "0.14.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index e3a4172e6..2be4a21df 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.9.2 - 2024-01-29 +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.9.1 - 2024-01-22 #### Bug Fixes - added partitions for 2025 and 2026 for audit table - (45d37dd) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 755d47ab8..1b3fef7f1 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.9.1" +version = "0.9.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 1c678b90b1017c177fc276d714a3ae28f684dc72 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Sun, 21 Jan 2024 22:12:44 +0530 Subject: [PATCH 249/352] fix: refactored experiment page and fixed experiment edit flow --- crates/frontend/src/api.rs | 25 +- .../components/context_form/context_form.rs | 30 +- .../src/components/context_form/utils.rs | 59 +- .../default_config_form.rs | 22 +- .../dimension_form/dimension_form.rs | 3 +- .../src/components/experiment/experiment.rs | 216 ++++++ .../frontend/src/components/experiment/mod.rs | 2 + .../src/components/experiment/utils.rs | 26 + .../experiment_conclude_form.rs | 95 +++ .../experiment_conclude_form/mod.rs | 2 + .../experiment_conclude_form/utils.rs | 29 + .../experiment_form/experiment_form.rs | 24 +- .../src/components/experiment_form/types.rs | 13 +- .../src/components/experiment_form/utils.rs | 43 +- .../experiment_ramp_form.rs | 53 ++ .../components/experiment_ramp_form/mod.rs | 2 + .../components/experiment_ramp_form/utils.rs | 29 + crates/frontend/src/components/mod.rs | 3 + crates/frontend/src/components/modal/modal.rs | 6 +- .../components/override_form/override_form.rs | 10 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 92 ++- crates/frontend/src/pages/Experiment/mod.rs | 620 +++--------------- crates/frontend/src/types.rs | 35 +- crates/frontend/src/utils.rs | 75 +++ crates/frontend/styles/tailwind.css | 6 + 25 files changed, 894 insertions(+), 626 deletions(-) create mode 100644 crates/frontend/src/components/experiment/experiment.rs create mode 100644 crates/frontend/src/components/experiment/mod.rs create mode 100644 crates/frontend/src/components/experiment/utils.rs create mode 100644 crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs create mode 100644 crates/frontend/src/components/experiment_conclude_form/mod.rs create mode 100644 crates/frontend/src/components/experiment_conclude_form/utils.rs create mode 100644 crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs create mode 100644 crates/frontend/src/components/experiment_ramp_form/mod.rs create mode 100644 crates/frontend/src/components/experiment_ramp_form/utils.rs diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index bb1cb3ca7..c4a1ee80d 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,6 +1,8 @@ use leptos::{server, ServerFnError}; -use crate::types::{Config, DefaultConfig, Dimension, ExperimentsResponse, ListFilters}; +use crate::types::{ + Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, ListFilters, +}; #[server(GetDimensions, "/fxn", "GetJson")] pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFnError> { @@ -98,3 +100,24 @@ pub async fn fetch_config(tenant: String) -> Result<Config, ServerFnError> { Err(e) => Err(ServerFnError::ServerError(e.to_string())), } } + +#[server(GetExperiment, "/fxn", "GetJson")] +pub async fn fetch_experiment( + exp_id: String, + tenant: String, +) -> Result<Experiment, ServerFnError> { + let client = reqwest::Client::new(); + let host = "http://localhost:8080"; + let url = format!("{}/experiments/{}", host, exp_id); + + match client.get(url).header("x-tenant", tenant).send().await { + Ok(experiment) => { + let experiment = experiment + .json() + .await + .map_err(|err| ServerFnError::ServerError(err.to_string()))?; + Ok(experiment) + } + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + } +} diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 42971fb61..205bdb4d6 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -10,6 +10,7 @@ pub fn context_form<NF>( dimensions: Vec<Dimension>, is_standalone: bool, context: Vec<(String, String, String)>, + #[prop(default = false)] disabled: bool, ) -> impl IntoView where NF: Fn(Vec<(String, String, String)>) + 'static, @@ -35,6 +36,12 @@ where handle_change(f_context.clone()); }); + let add_dropdown_class = if disabled { + "dropdown dropdown-left disable-click" + } else { + "dropdown dropdown-left" + }; + view! { <div> <div class="form-control w-full "> @@ -43,7 +50,7 @@ where <span class="label-text font-semibold text-base">Context</span> </label> <div> - <div class="dropdown dropdown-left"> + <div class=add_dropdown_class> <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> <i class="ri-add-line"></i> Add Context @@ -126,7 +133,8 @@ where <span class="label-text">Operator</span> </label> <select - bind:value=operator + disabled=disabled + value=operator.clone() on:input=move |event| { let input_value = event_target_value(&event); set_context @@ -141,9 +149,15 @@ where <option disabled selected> Pick one </option> - <option value="==">"IS"</option> - <option value="IN">"HAS"</option> - <option value="<=">"BETWEEN (inclusive)"</option> + <option value="==" selected=operator.clone() == "=="> + "IS" + </option> + <option value="IN" selected=operator.clone() == "IN"> + "HAS" + </option> + <option value="<=" selected=operator.clone() == "<="> + "BETWEEN (inclusive)" + </option> </select> </div> @@ -155,7 +169,8 @@ where </label> <div class="flex gap-x-6 items-center"> <input - bind:value=value + disabled=disabled + value=value on:input=move |event| { let input_value = event_target_value(&event); set_context @@ -171,6 +186,7 @@ where /> <button class="btn btn-ghost btn-circle btn-sm" + disabled=disabled on:click=move |_| { set_context .update(|value| { @@ -209,7 +225,7 @@ where </div> <Show when=move || is_standalone> <div class="flex justify-end"> - <button class="btn" on:click:undelegated=on_click> + <button class="btn" on:click:undelegated=on_click disabled=disabled> Save </button> </div> diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 08d429408..621e96632 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -2,31 +2,46 @@ use crate::utils::get_host; use reqwest::StatusCode; use serde_json::{json, Map, Value}; +pub fn get_condition_schema(var: &str, op: &str, val: &str) -> Value { + match op { + "<=" => { + let mut split_value = val.split(','); + let first_operand = + split_value.next().unwrap().trim().parse::<i64>().unwrap(); + let second_operand = + split_value.next().unwrap().trim().parse::<i64>().unwrap(); + + json!({ + op: [ + first_operand, + { "var": var }, + second_operand + ] + }) + } + _ => { + json!({ + op: [ + {"var": var}, + val + ] + }) + } + } +} + pub fn construct_context(conditions: Vec<(String, String, String)>) -> Value { - let context = if conditions.len() == 1 { - // Single condition - let (variable, operator, value) = &conditions[0]; - json!({ - operator: [ - { "var": variable }, - value - ] + let condition_schemas = conditions + .iter() + .map(|(variable, operator, value)| { + get_condition_schema(variable, operator, value) }) - } else { - // Multiple conditions inside an "and" - let and_conditions: Vec<Value> = conditions - .into_iter() - .map(|(variable, operator, value)| { - json!({ - operator: [ - { "var": variable }, - value - ] - }) - }) - .collect(); + .collect::<Vec<Value>>(); - json!({ "and": and_conditions }) + let context = if condition_schemas.len() == 1 { + condition_schemas[0].clone() + } else { + json!({ "and": condition_schemas }) }; context diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 73f58bbb5..3e3c7f6b5 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -110,6 +110,7 @@ where set_config_key.set(value); } /> + </div> <select @@ -141,13 +142,19 @@ where Set Schema </option> - <option value="number" selected=move || { config_type.get() == "number".to_string() }> + <option + value="number" + selected=move || { config_type.get() == "number".to_string() } + > "Number" </option> <option value="enum" selected=move || { config_type.get() == "enum".to_string() }> "String (Enum)" </option> - <option value="pattern" selected=move || { config_type.get() == "pattern".to_string() }> + <option + value="pattern" + selected=move || { config_type.get() == "pattern".to_string() } + > "String (regex)" </option> <option value="other" selected=move || { config_type.get() == "other".to_string() }> @@ -168,12 +175,11 @@ where class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=config_value.get() on:change=move |ev| { - logging::log!( - "{:?}", event_target_value(&ev) - ); + logging::log!("{:?}", event_target_value(& ev)); set_config_value.set(event_target_value(&ev)); } /> + </div> </Show> @@ -188,12 +194,11 @@ where class="input input-bordered w-full bg-white text-gray-700 shadow-md" value=config_value.get() on:change=move |ev| { - logging::log!( - "{:?}", event_target_value(&ev) - ); + logging::log!("{:?}", event_target_value(& ev)); set_config_value.set(event_target_value(&ev)); } /> + </div> <div class="form-control"> <label class="label font-mono"> @@ -210,6 +215,7 @@ where set_config_pattern.set(value); } > + {config_pattern.get()} </textarea> diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index 824f77447..6db1eda77 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -135,8 +135,9 @@ where <option value="number" - selected=move || { logging::log!("{:?}", dimension_type.get()); dimension_type.get() == "number".to_string() } + selected=move || { dimension_type.get() == "number".to_string() } > + "Number" </option> <option diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs new file mode 100644 index 000000000..2cd703fd2 --- /dev/null +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -0,0 +1,216 @@ +use std::rc::Rc; + +use leptos::*; + +use crate::components::condition_pills::utils::extract_and_format; +use crate::components::table::table::Table; +use crate::components::table::types::Column; + +use super::utils::gen_variant_rows; +use crate::types::{Experiment, ExperimentStatusType}; + +#[component] +pub fn experiment<HS, HR, HC, HE>( + experiment: Experiment, + handle_start: HS, + handle_ramp: HR, + handle_conclude: HC, + handle_edit: HE, +) -> impl IntoView +where + HS: Fn(String) + 'static + Clone, + HR: Fn() + 'static + Clone, + HC: Fn() + 'static + Clone, + HE: Fn() + 'static + Clone, +{ + let experiment_rc = Rc::new(experiment.clone()); + let contexts = extract_and_format(&experiment_rc.clone().context); + + view! { + <div class="flex flex-col overflow-x-auto p-2 bg-transparent"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let class_name = match exp.status { + ExperimentStatusType::CREATED => { + "badge text-white ml-3 mb-1 badge-xl badge-info" + } + ExperimentStatusType::INPROGRESS => { + "badge text-white ml-3 mb-1 badge-xl badge-warning" + } + ExperimentStatusType::CONCLUDED => { + "badge text-white ml-3 mb-1 badge-xl badge-success" + } + }; + view! { + <h1 class="text-2xl pt-4 font-extrabold"> + {&exp.name} <span class=class_name>{exp.status.to_string()}</span> + </h1> + } + } + } + <div class="divider"></div> + <div class="flex flex-row justify-end join m-5"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let handle_start = handle_start.clone(); + let handle_conclude = handle_conclude.clone(); + let handle_ramp = handle_ramp.clone(); + let handle_edit = handle_edit.clone(); + match exp.status { + ExperimentStatusType::CREATED => { + view! { + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_edit() } + > + + <i class="ri-edit-line"></i> + Edit + </button> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_start(exp.id.to_string()) } + > + + <i class="ri-guide-line"></i> + Start + </button> + } + .into_view() + } + ExperimentStatusType::INPROGRESS => { + view! { + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_conclude() } + > + + <i class="ri-stop-circle-line"></i> + Conclude + </button> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_ramp() } + > + + <i class="ri-flight-takeoff-line"></i> + Ramp + </button> + } + .into_view() + } + ExperimentStatusType::CONCLUDED => { + view! { + <div class="stat"> + <div class="stat-title">Chosen Variant</div> + <div class="stat-value"> + {match exp.chosen_variant { + Some(ref v) => format!("{}", v), + None => String::new(), + }} + + </div> + </div> + } + .into_view() + } + } + } + } + + </div> + <div class="flex bg-base-100 flex-row gap-2 justify-between flex-wrap shadow m-5"> + <div class="stat w-2/12"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value text-sm">{experiment.id}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value text-sm">{experiment.traffic_percentage}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Created by</div> + <div class="stat-value text-sm">{experiment.created_by}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Created at</div> + <div class="stat-value text-sm"> + {format!("{}", experiment.created_at.format("%v"))} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Last Modified</div> + <div class="stat-value text-sm"> + + {format!("{}", experiment.last_modified.format("%v"))} + + </div> + </div> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row flex-wrap gap-2"> + {move || { + let context = contexts.clone(); + let mut view = Vec::new(); + let tokens = context.split("&&"); + for token in tokens.into_iter() { + let mut t = token.trim().split(" "); + let (dimension, _, value) = (t.next(), t.next(), t.next()); + view.push( + view! { + <div class="stat w-3/12"> + <div class="stat-title"> + {format!("{}", dimension.unwrap())} + </div> + <div class="stat-value text-base"> + {format!("{}", &value.unwrap().replace("\"", ""))} + + </div> + </div> + }, + ); + } + view + }} + + </div> + </div> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto overflow-y-auto"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let rows = gen_variant_rows(&exp.variants).unwrap(); + let mut columns: Vec<Column> = Vec::new(); + columns.push(Column::default("Variant".into())); + for okey in exp.override_keys.as_array().unwrap().into_iter() { + columns.push(Column::default(okey.as_str().unwrap().into())); + } + view! { + <Table + table_style="abc".to_string() + rows=rows + key_column="overrides".to_string() + columns=columns + /> + } + } + } + + </div> + </div> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/experiment/mod.rs b/crates/frontend/src/components/experiment/mod.rs new file mode 100644 index 000000000..780b31f34 --- /dev/null +++ b/crates/frontend/src/components/experiment/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment; +pub mod utils; diff --git a/crates/frontend/src/components/experiment/utils.rs b/crates/frontend/src/components/experiment/utils.rs new file mode 100644 index 000000000..028886c17 --- /dev/null +++ b/crates/frontend/src/components/experiment/utils.rs @@ -0,0 +1,26 @@ +use crate::types::{Variant, VariantType}; +use serde_json::{Map, Value}; + +pub fn gen_variant_rows(variants: &[Variant]) -> Result<Vec<Map<String, Value>>, String> { + let rows = variants + .iter() + .enumerate() + .map(|(i, variant)| { + let variant_name = match variant.variant_type { + VariantType::CONTROL => "Control".into(), + VariantType::EXPERIMENTAL => format!("Variant-{i}"), + }; + let mut row_data = variant + .overrides + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect::<Vec<(String, Value)>>(); + row_data.extend_from_slice(&[ + (String::from("Variant"), variant_name.into()), + (String::from("variant_id"), variant.id.clone().into()), + ]); + Map::from_iter(row_data) + }) + .collect::<Vec<Map<String, Value>>>(); + Ok(rows) +} diff --git a/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs b/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs new file mode 100644 index 000000000..0453bb4ac --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs @@ -0,0 +1,95 @@ +use std::rc::Rc; + +use super::utils::conclude_experiment; +use crate::types::{Experiment, Variant, VariantType}; +use leptos::*; + +#[component] +pub fn experiment_conclude_form<HS>( + experiment: Experiment, + handle_submit: HS, +) -> impl IntoView +where + HS: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let experiment_rc = Rc::new(experiment); + + let experiment_clone = experiment_rc.clone(); + let handle_conclude_experiment = move |variant_id: String| { + let handle_submit_clone = handle_submit.clone(); + spawn_local(async move { + let experiment = experiment_clone.clone(); + let tenant = tenant_rs.get(); + let _ = + conclude_experiment(experiment.id.to_string(), variant_id, &tenant).await; + handle_submit_clone(); + }) + }; + + view! { + <h3 class="font-bold text-lg">Conclude This Experiment</h3> + <p class="py-4"> + Choose a variant to conclude with, this variant becomes + the new default that is served to requests that match this context + </p> + <form method="dialog"> + <For + each=move || { + experiment_rc + .clone() + .variants + .clone() + .into_iter() + .enumerate() + .collect::<Vec<(usize, Variant)>>() + } + + key=|(_, variant)| variant.id.to_string() + children=move |(idx, variant)| { + let variant = variant.clone(); + let variant_type = variant.variant_type; + let variant_id = variant.id; + let handle_conclude_experiment_clone = handle_conclude_experiment.clone(); + match variant_type { + VariantType::CONTROL => { + view! { + <button + class="btn btn-block btn-outline btn-info m-2" + on:click=move |_| { + let handle_conclude_experiment_clone = handle_conclude_experiment_clone + .clone(); + handle_conclude_experiment_clone(variant_id.to_string()) + } + > + + Control + </button> + } + .into_view() + } + VariantType::EXPERIMENTAL => { + { + view! { + <button + class="btn btn-block btn-outline btn-success m-2" + on:click=move |_| { + let handle_conclude_experiment_clone = handle_conclude_experiment_clone + .clone(); + handle_conclude_experiment_clone(variant_id.to_string()) + } + > + + {format!("Variant-{idx}")} + </button> + } + } + .into_view() + } + } + } + /> + + </form> + } +} diff --git a/crates/frontend/src/components/experiment_conclude_form/mod.rs b/crates/frontend/src/components/experiment_conclude_form/mod.rs new file mode 100644 index 000000000..1205d1162 --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_conclude_form; +pub mod utils; diff --git a/crates/frontend/src/components/experiment_conclude_form/utils.rs b/crates/frontend/src/components/experiment_conclude_form/utils.rs new file mode 100644 index 000000000..b73829c95 --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/utils.rs @@ -0,0 +1,29 @@ +use leptos::logging::log; +use serde_json::json; + +use crate::{types::Experiment, utils::get_host}; + +pub async fn conclude_experiment( + exp_id: String, + variant_id: String, + tenant: &String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + let host = get_host(); + match client + .patch(format!("{host}/experiments/{}/conclude", exp_id)) + .header("x-tenant", tenant) + .json(&json!({ "chosen_variant": variant_id })) + .send() + .await + { + Ok(experiment) => { + log!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 2365d1de5..3584871f4 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,4 +1,4 @@ -use super::utils::create_experiment; +use super::utils::{create_experiment, update_experiment}; use crate::components::button::button::Button; use crate::components::{ context_form::context_form::ContextForm, override_form::override_form::OverrideForm, @@ -6,7 +6,6 @@ use crate::components::{ use crate::types::{DefaultConfig, Dimension, Variant, VariantType}; use chrono::offset::Local; use leptos::*; -use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use web_sys::MouseEvent; @@ -35,14 +34,10 @@ fn default_variants_for_form() -> Vec<(String, Variant)> { ] } -#[derive(Serialize, Deserialize, Clone, Debug)] -struct CombinedResource { - dimensions: Vec<Dimension>, - default_config: Vec<DefaultConfig>, -} - #[component] pub fn experiment_form<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] id: String, name: String, context: Vec<(String, String, String)>, variants: Vec<Variant>, @@ -94,6 +89,7 @@ where .map(|(_, variant)| variant) .collect::<Vec<Variant>>(); let tenant = tenant_rs.get(); + let experiment_id = id.clone(); let handle_submit_clone = handle_submit.clone(); logging::log!("{:?}", f_experiment_name); @@ -101,9 +97,12 @@ where spawn_local({ async move { - let result = + let result = if edit { + update_experiment(experiment_id, f_variants, tenant).await + } else { create_experiment(f_context, f_variants, f_experiment_name, tenant) - .await; + .await + }; match result { Ok(_) => { @@ -125,6 +124,7 @@ where <span class="label-text">Name</span> </label> <input + disabled=edit value=move || experiment_name.get() on:input=move |ev| set_experiment_name.set(event_target_value(&ev)) type="text" @@ -141,6 +141,7 @@ where context=context handle_change=handle_context_form_change is_standalone=false + disabled=edit /> </div> @@ -152,6 +153,7 @@ where <button class="btn btn-outline btn-sm text-xs m-1" + disabled=edit on:click:undelegated=move |_| { leptos::logging::log!("add new variant"); set_variants @@ -201,6 +203,7 @@ where <input name="variantId" value=move || variant.id.to_string() + disabled=edit type="text" placeholder="Type a unique name here" class="input input-bordered w-full max-w-xs" @@ -213,6 +216,7 @@ where <select name="expType[]" value=variant.variant_type.to_string() + disabled=edit on:change=move |ev| { let mut new_variant = variant_clone.clone(); new_variant diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs index a84f13183..8efc20222 100644 --- a/crates/frontend/src/components/experiment_form/types.rs +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -1,6 +1,6 @@ use crate::types::Variant; use serde::Serialize; -use serde_json::Value; +use serde_json::{Map, Value}; #[derive(Serialize)] pub struct ExperimentCreateRequest { @@ -9,3 +9,14 @@ pub struct ExperimentCreateRequest { pub context: Value, pub variants: Vec<Variant>, } + +#[derive(Serialize, Debug)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: Map<String, Value>, +} + +#[derive(Serialize, Debug)] +pub struct ExperimentUpdateRequest { + pub variants: Vec<VariantUpdateRequest>, +} diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index a9f06d3b8..4e8b92cc0 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -1,4 +1,6 @@ -use super::types::ExperimentCreateRequest; +use super::types::{ + ExperimentCreateRequest, ExperimentUpdateRequest, VariantUpdateRequest, +}; use crate::components::context_form::utils::construct_context; use crate::types::Variant; use crate::utils::get_host; @@ -12,8 +14,8 @@ pub async fn create_experiment( tenant: String, ) -> Result<String, String> { let payload = ExperimentCreateRequest { - name: name, - variants: variants, + name, + variants, context: construct_context(conditions), }; @@ -35,3 +37,38 @@ pub async fn create_experiment( _ => Err("Internal Server Error".to_string()), } } + +pub async fn update_experiment( + experiment_id: String, + variants: Vec<Variant>, + tenant: String, +) -> Result<String, String> { + let payload = ExperimentUpdateRequest { + variants: variants + .into_iter() + .map(|variant| VariantUpdateRequest { + id: variant.id, + overrides: variant.overrides, + }) + .collect::<Vec<VariantUpdateRequest>>(), + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{}/experiments/{}/overrides", host, experiment_id); + let request_payload = json!(payload); + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs b/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs new file mode 100644 index 000000000..539113855 --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs @@ -0,0 +1,53 @@ +use std::rc::Rc; + +use leptos::{logging::log, *}; +use web_sys::MouseEvent; + +use super::utils::ramp_experiment; +use crate::{components::button::button::Button, types::Experiment}; + +#[component] +pub fn experiment_ramp_form<NF>( + experiment: Experiment, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let (traffic, set_traffic) = create_signal(experiment.traffic_percentage); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let experiment_rc = Rc::new(experiment); + let handle_ramp_experiment = move |event: MouseEvent| { + event.prevent_default(); + let experiment_clone = experiment_rc.clone(); + let handle_submit_clone = handle_submit.clone(); + spawn_local(async move { + let tenant = tenant_rs.get(); + let traffic_value = traffic.get(); + let _ = ramp_experiment(&experiment_clone.id, traffic_value, &tenant).await; + handle_submit_clone() + }); + }; + view! { + <h3 class="font-bold text-lg">Ramp up with release</h3> + <p class="py-4">Increase the traffic being redirected to the variants</p> + <form> + <p>{move || traffic.get()}</p> + <input + type="range" + min="0" + max="100" + value=move || traffic.get() + class="range" + on:input=move |event| { + let traffic_value = event_target_value(&event).parse::<u8>().unwrap(); + log!("traffic value:{traffic_value}"); + set_traffic.set(traffic_value); + } + /> + + <Button text="Set".to_string() on_click=handle_ramp_experiment/> + </form> + } +} diff --git a/crates/frontend/src/components/experiment_ramp_form/mod.rs b/crates/frontend/src/components/experiment_ramp_form/mod.rs new file mode 100644 index 000000000..706bb7d5d --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_ramp_form; +pub mod utils; diff --git a/crates/frontend/src/components/experiment_ramp_form/utils.rs b/crates/frontend/src/components/experiment_ramp_form/utils.rs new file mode 100644 index 000000000..7018ad619 --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/utils.rs @@ -0,0 +1,29 @@ +use leptos::logging::log; +use serde_json::json; + +use crate::{types::Experiment, utils::get_host}; + +pub async fn ramp_experiment( + exp_id: &String, + percent: u8, + tenant: &String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + let host = get_host(); + match client + .patch(format!("{host}/experiments/{}/ramp", exp_id)) + .header("x-tenant", tenant) + .json(&json!({ "traffic_percentage": percent })) + .send() + .await + { + Ok(experiment) => { + log!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 68e0b1c80..00c0794c8 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -3,7 +3,10 @@ pub mod condition_pills; pub mod context_form; pub mod default_config_form; pub mod dimension_form; +pub mod experiment; +pub mod experiment_conclude_form; pub mod experiment_form; +pub mod experiment_ramp_form; pub mod modal; pub mod nav_item; pub mod override_form; diff --git a/crates/frontend/src/components/modal/modal.rs b/crates/frontend/src/components/modal/modal.rs index 402b77390..b2dd46da3 100644 --- a/crates/frontend/src/components/modal/modal.rs +++ b/crates/frontend/src/components/modal/modal.rs @@ -11,10 +11,10 @@ pub fn modal<NF>( where NF: Fn() + 'static + Clone, { - let classnames = format!("modal modal-middle {classnames}"); + let classnames = format!("modal-box {classnames}"); view! { - <dialog id=id class=classnames> - <div class="modal-box"> + <dialog id=id class="modal modal-middle"> + <div class=classnames> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" on:click=move |_| { handle_close() } diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 8897826ba..4fc9ccde6 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -15,8 +15,13 @@ where NF: Fn(Map<String, Value>) + 'static, { let has_default_config = default_config.len() != 0; + let (used_config_keys, set_used_config_keys) = create_signal( + overrides + .keys() + .map(String::from) + .collect::<HashSet<String>>(), + ); let (overrides, set_overrides) = create_signal(overrides.clone()); - let (used_config_keys, set_used_config_keys) = create_signal(HashSet::new()); let on_submit = move |event: MouseEvent| { event.prevent_default(); @@ -99,6 +104,7 @@ where children=move |(config_key, config_value)| { let config_key_label = config_key.to_string(); let config_key_value = config_key.to_string(); + let config_value = config_value.to_string().replace("\"", ""); view! { <div> <div class="flex items-center gap-4"> @@ -113,7 +119,7 @@ where placeholder="Enter override here" name="override" class="input input-bordered w-full bg-white text-gray-700 shadow-md" - bind:value=config_value.to_string() + value=config_value on:input=move |event| { let input_value = event_target_value(&event); set_overrides diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 0ade94ce5..8f77cf907 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -161,61 +161,59 @@ pub fn DefaultConfig() -> impl IntoView { } } }} + </Modal> <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - { - move || { - let default_config = default_config_resource.get().unwrap_or(vec![]); - let total_default_config_keys = default_config.len().to_string(); - let table_rows = default_config - .into_iter() - .map(|config| { - let mut ele_map = json!(config).as_object().unwrap().to_owned(); - ele_map - .insert( - "created_at".to_string(), - json!(config.created_at.format("%v").to_string()), - ); - ele_map - }) - .collect::<Vec<Map<String, Value>>>(); - view! { - <div class="pb-4"> - <Stat - heading="Config Keys" - icon="ri-tools-line" - number=total_default_config_keys - /> - </div> - <div class="card rounded-lg w-full bg-base-100 shadow"> - <div class="card-body"> - <div class="flex justify-between"> - <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> - "Default Config" - </h2> - <Button - text="Create Key".to_string() - on_click={ - move |_| { - show_modal("default_config_modal_form") - } - } - /> - - </div> - <Table - table_style="font-mono".to_string() - rows=table_rows - key_column="id".to_string() - columns=table_columns.get() + + {move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let total_default_config_keys = default_config.len().to_string(); + let table_rows = default_config + .into_iter() + .map(|config| { + let mut ele_map = json!(config).as_object().unwrap().to_owned(); + ele_map + .insert( + "created_at".to_string(), + json!(config.created_at.format("%v").to_string()), + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat + heading="Config Keys" + icon="ri-tools-line" + number=total_default_config_keys + /> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Default Config" + </h2> + <Button + text="Create Key".to_string() + on_click=move |_| { + show_modal("default_config_modal_form") + } /> + </div> + <Table + table_style="font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + /> </div> - } + </div> } - } + }} </Suspense> </div> diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 6e4b10e58..00adf2fcb 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -1,130 +1,27 @@ -use chrono::{DateTime, Utc}; -use leptos::{html::Input, logging::log, *}; +use futures::join; +use leptos::*; use leptos_router::use_params_map; use serde::{Deserialize, Serialize}; -use serde_json::{json, Map, Value}; -use web_sys::SubmitEvent; use crate::{ - api::{fetch_default_config, fetch_dimensions}, + api::{fetch_default_config, fetch_dimensions, fetch_experiment}, components::{ - condition_pills::utils::extract_and_format, - table::{table::Table, types::Column}, + experiment::experiment::Experiment, + experiment_conclude_form::experiment_conclude_form::ExperimentConcludeForm, + experiment_form::experiment_form::ExperimentForm, + experiment_ramp_form::utils::ramp_experiment, modal::modal::Modal, }, - utils::get_host, + types::{DefaultConfig, Dimension, Experiment}, + utils::{close_modal, extract_conditions, show_modal}, }; -#[derive( - Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, -)] -#[strum(serialize_all = "UPPERCASE")] -pub(crate) enum ExperimentStatusType { - CREATED, - INPROGRESS, - CONCLUDED, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, strum_macros::Display)] -#[strum(serialize_all = "UPPERCASE")] -pub(crate) enum VariantType { - CONTROL, - EXPERIMENTAL, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct Variant { - pub id: String, - pub override_id: String, - pub context_id: String, - pub overrides: Value, - pub(crate) variant_type: VariantType, -} - -pub type Variants = Vec<Variant>; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Experiment { - pub(crate) variants: Variants, - pub(crate) name: String, - pub(crate) id: String, - pub(crate) traffic_percentage: u8, - pub(crate) context: Value, - pub(crate) status: ExperimentStatusType, - pub(crate) override_keys: Value, - pub(crate) created_by: String, - pub(crate) created_at: DateTime<Utc>, - pub(crate) last_modified: DateTime<Utc>, - pub(crate) chosen_variant: Option<String>, -} +use crate::components::experiment_ramp_form::experiment_ramp_form::ExperimentRampForm; -async fn get_experiment(exp_id: &String, tenant: &String) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - let host = get_host(); - match client - .get(format!("{host}/experiments/{}", exp_id)) - .header("x-tenant", tenant) - .send() - .await - { - Ok(experiment) => { - log!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } -} - -async fn ramp_experiment( - exp_id: &String, - percent: u8, - tenant: &String, -) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - let host = get_host(); - match client - .patch(format!("{host}/experiments/{}/ramp", exp_id)) - .header("x-tenant", tenant) - .json(&json!({ "traffic_percentage": percent })) - .send() - .await - { - Ok(experiment) => { - log!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } -} - -async fn conclude_experiment( - exp_id: String, - variant_id: String, - tenant: &String, -) -> Result<Experiment, String> { - let client = reqwest::Client::new(); - let host = get_host(); - match client - .patch(format!("{host}/experiments/{}/conclude", exp_id)) - .header("x-tenant", tenant) - .json(&json!({ "chosen_variant": variant_id })) - .send() - .await - { - Ok(experiment) => { - log!("experiment response {:?}", experiment); - Ok(experiment - .json::<Experiment>() - .await - .map_err(|err| err.to_string())?) - } - Err(e) => Err(e.to_string()), - } +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + experiment: Option<Experiment>, + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, } #[component] @@ -138,411 +35,110 @@ pub fn experiment_page() -> impl IntoView { (exp_id, t) }; - let experiment_info = create_resource(source, |(exp_id, tenant)| async move { - get_experiment(&exp_id, &tenant).await - }); - view! { - <Transition fallback=move || { - view! { <h1>Loading....</h1> } - }> - {move || match experiment_info.get() { - Some(Ok(experiment)) => { - let tenant = tenant_rs.get(); - experiment_detail_view(tenant, experiment, experiment_info).into_view() - } - Some(Err(err)) => view! { <h1>{err.to_string()}</h1> }.into_view(), - None => view! { <h1>No elements</h1> }.into_view(), - }} + let combined_resource: Resource<(String, String), CombinedResource> = + create_blocking_resource(source, |(exp_id, tenant)| async move { + // Perform all fetch operations concurrently + let experiments_future = + fetch_experiment(exp_id.to_string(), tenant.to_string()); + let dimensions_future = fetch_dimensions(tenant.to_string()); + let config_future = fetch_default_config(tenant.to_string()); + + let (experiments_result, dimensions_result, config_result) = + join!(experiments_future, dimensions_future, config_future); + + // Construct the combined result, handling errors as needed + CombinedResource { + experiment: experiments_result.ok(), + dimensions: dimensions_result.unwrap_or_else(|_| vec![]), + default_config: config_result.unwrap_or_else(|_| vec![]), + } + }); - </Transition> - } -} + let handle_start = move |experiment_id: String| { + spawn_local(async move { + let tenant = tenant_rs.get(); + let _ = ramp_experiment(&experiment_id, 1, &tenant).await; + combined_resource.refetch(); + }) + }; -fn experiment_detail_view( - tenant: String, - initial_data: Experiment, - exp_resource: Resource<(String, String), Result<Experiment, String>>, -) -> impl IntoView { - let contexts = extract_and_format(&initial_data.context); - let (experiment, _) = create_signal(initial_data); - let (ctxs, _) = create_signal(contexts); + let handle_ramp = move || show_modal("ramp_form_modal"); + let handle_conclude = move || show_modal("conclude_form_modal"); + let handle_edit = move || show_modal("experiment_edit_form_modal"); view! { - <div class="flex flex-col overflow-x-auto p-2 bg-transparent"> + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> {move || { - experiment - .with(|exp| { - let class_name = match exp.status { - ExperimentStatusType::CREATED => { - "badge text-white ml-3 mb-1 badge-xl badge-info" - } - ExperimentStatusType::INPROGRESS => { - "badge text-white ml-3 mb-1 badge-xl badge-warning" - } - ExperimentStatusType::CONCLUDED => { - "badge text-white ml-3 mb-1 badge-xl badge-success" - } - }; + let resource = match combined_resource.get() { + Some(res) => res, + None => return view! { <h1>Error fetching experiment</h1> }.into_view(), + }; + let experiment = resource.experiment; + let default_config = resource.default_config; + let dimensions = resource.dimensions; + match experiment { + Some(experiment) => { + let experiment_rf = experiment.clone(); + let experiment_cf = experiment.clone(); + let experiment_ef = experiment.clone(); view! { - <h1 class="text-2xl pt-4 font-extrabold"> - {&exp.name} <span class=class_name>{exp.status.to_string()}</span> - </h1> - } - }) - }} - <div class="divider"></div> - <div class="flex flex-row justify-end join m-5"> - {move || { - experiment - .with(|exp| { - let tenant_clone = tenant.clone(); - match exp.status { - ExperimentStatusType::CREATED => { - view! { - <button - class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" - onclick="edit_exp_modal.showModal()" - > - <i class="ri-edit-line"></i> - Edit - </button> - <button - class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" - value=exp.id.to_string() - on:click=move |button_event| { - let tenant_clone = tenant_clone.clone(); - spawn_local(async move { - let tenant_clone = tenant_clone.clone(); - let value = event_target_value(&button_event); - let _ = ramp_experiment(&value, 1, &tenant_clone).await; - exp_resource.refetch(); - }) - } - > - - <i class="ri-guide-line"></i> - Start - </button> - } - } - ExperimentStatusType::INPROGRESS => { - view! { - <button - class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" - onclick="conclude_exp_modal.showModal()" - > - <i class="ri-stop-circle-line"></i> - Conclude - </button> - <button - class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" - onclick="ramp_exp_modal.showModal()" - > - <i class="ri-flight-takeoff-line"></i> - Ramp - </button> - } - } - ExperimentStatusType::CONCLUDED => { - view! { - <></> - <div class="stat"> - <div class="stat-title">Chosen Variant</div> - <div class="stat-value"> - {match exp.chosen_variant { - Some(ref v) => format!("{}", v), - None => String::new(), - }} - - </div> - </div> - } - } - } - }) - }} - - </div> - <div class="flex bg-base-100 flex-row gap-2 justify-between flex-wrap shadow m-5"> - <div class="stat w-2/12"> - <div class="stat-title">Experiment ID</div> - <div class="stat-value text-sm">{experiment.get().id}</div> - </div> - <div class="stat w-2/12"> - <div class="stat-title">Current Traffic Percentage</div> - <div class="stat-value text-sm"> - {move || experiment.get().traffic_percentage} - </div> - </div> - <div class="stat w-2/12"> - <div class="stat-title">Created by</div> - <div class="stat-value text-sm">{experiment.get().created_by}</div> - </div> - <div class="stat w-2/12"> - <div class="stat-title">Created at</div> - <div class="stat-value text-sm"> - {format!("{}", experiment.get().created_at.format("%v"))} - </div> - </div> - <div class="stat w-2/12"> - <div class="stat-title">Last Modified</div> - <div class="stat-value text-sm"> - {move || { - experiment.with(|exp| format!("{}", &exp.last_modified.format("%v"))) - }} - - </div> - </div> - </div> <div class="card bg-base-100 max-w-screen shadow m-5"> - <div class="card-body"> - <h2 class="card-title">Context</h2> - <div class="flex flex-row flex-wrap gap-2"> - {move || { - let context = ctxs.get(); - let mut view = Vec::new(); - let tokens = context.split("&&"); - for token in tokens.into_iter() { - let mut t = token.trim().split(" "); - let (dimension, _, value) = (t.next(), t.next(), t.next()); - view.push( - view! { - <div class="stat w-3/12"> - <div class="stat-title"> - {format!("{}", dimension.unwrap())} - </div> - <div class="stat-value text-base"> - {format!( - "{}", - &value.unwrap()[1..value.unwrap().chars().count() - 1], - )} - - </div> - </div> - }, - ); - } - view - }} - - </div> - </div> - </div> <div class="card bg-base-100 max-w-screen shadow m-5"> - <div class="card-body"> - <h2 class="card-title">Variants</h2> - <div class="overflow-x-auto overflow-y-auto"> - {move || { - let exp = move || experiment.get(); - let rows = gen_variant_rows(&exp().variants).unwrap(); - let mut columns: Vec<Column> = Vec::new(); - columns.push(Column::default("Variant".into())); - for okey in exp().override_keys.as_array().unwrap().into_iter() { - columns.push(Column::default(okey.as_str().unwrap().into())); - } - view! { - <Table - table_style="abc".to_string() - rows=rows - key_column="overrides".to_string() - columns=columns + <Experiment + experiment=experiment.clone() + handle_start=handle_start + handle_ramp=handle_ramp + handle_conclude=handle_conclude + handle_edit=handle_edit + /> + <Modal + id="ramp_form_modal".to_string() + handle_close=move || { close_modal("ramp_form_modal") } + > + + <ExperimentRampForm + experiment=experiment_rf + handle_submit=move || { combined_resource.refetch() } /> - } - }} - - </div> - </div> - </div> - </div> - {add_dialogs(experiment, exp_resource)} - } -} - -fn gen_variant_rows(variants: &Vec<Variant>) -> Result<Vec<Map<String, Value>>, String> { - let mut rows: Vec<Map<String, Value>> = Vec::new(); - for (i, variant) in variants.into_iter().enumerate() { - let variant_name = match variant.variant_type { - VariantType::CONTROL => "Control".into(), - VariantType::EXPERIMENTAL => format!("Variant-{i}"), - }; - let mut m = Map::new(); - m.insert("Variant".into(), variant_name.into()); - m.insert("variant_id".into(), variant.id.clone().into()); - for (o, value) in variant.overrides.as_object().unwrap().into_iter() { - m.insert(o.clone(), value.clone()); - } - rows.push(m); - } - Ok(rows) -} - -fn add_dialogs( - experiment_rs: ReadSignal<Experiment>, - experiment_ws: Resource<(String, String), Result<Experiment, String>>, -) -> impl IntoView { - let input_element: NodeRef<Input> = create_node_ref(); - let experiment = move || experiment_rs.get(); - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let (traffic, set_traffic) = create_signal(experiment().traffic_percentage); - let on_submit = move |ev: SubmitEvent| { - ev.prevent_default(); - let value = input_element - .get() - .expect("<input> to exist") - .value_as_number() as u8; - spawn_local(async move { - let tenant = tenant_rs.get(); - let _ = ramp_experiment(&experiment().id, value, &tenant).await; - experiment_ws.refetch(); - }); - }; - - let _dimensions = create_resource( - move || tenant_rs.get(), - |tenant| async { - match fetch_dimensions(tenant).await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); - - let _default_config = create_resource( - move || tenant_rs.get(), - |tenant| async { - match fetch_default_config(tenant).await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); + </Modal> + <Modal + id="conclude_form_modal".to_string() + handle_close=move || { close_modal("conclude_form_modal") } + > - match experiment_rs.get().status { - ExperimentStatusType::CREATED => view! { - <dialog id="edit_exp_modal" class="modal"> - <div class="modal-box"> - <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> - <i class="ri-close-line"></i> - </button> - </form> - <h3 class="font-bold text-lg">Edit Experiment</h3> - // <ExperimentForm - // name=experiment_rs.get().name - // context=vec![] - // variants=vec![] - // dimensions=dimensions.get().unwrap_or(vec![]) - // default_config=default_config.get().unwrap_or(vec![]) - // /> - <div class="modal-action"></div> - </div> - </dialog> - } - .into_view(), - ExperimentStatusType::INPROGRESS => view! { - <dialog id="conclude_exp_modal" class="modal"> - <div class="modal-box"> - <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> - <i class="ri-close-line"></i> - </button> - </form> - - <h3 class="font-bold text-lg">Conclude This Experiment</h3> - <p class="py-4"> - Choose a variant to conclude with, this variant becomes - the new default that is served to requests that match this context - </p> - <form method="dialog"> - {move || { - let mut view_arr = vec![]; - let tenant = tenant_rs.get(); - for (i, v) in experiment_rs.get().variants.into_iter().enumerate() { - let (variant, _) = create_signal(v); - let tenant_clone = tenant.clone(); - let view = match variant.get().variant_type { - VariantType::CONTROL => { - view! { - <button - class="btn btn-block btn-outline btn-info m-2" - on:click=move |_| { - let tenant_clone = tenant_clone.clone(); - spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone(), &tenant_clone) - .await - .unwrap(); - experiment_ws.refetch(); - }) - } - > - - Control - </button> - } - } - VariantType::EXPERIMENTAL => { - view! { - <button - class="btn btn-block btn-outline btn-success m-2" - on:click=move |_| { - let tenant_clone = tenant_clone.clone(); - spawn_local(async move { - let e = experiment_rs.get(); - let variant = variant.get(); - conclude_experiment(e.id, variant.id.clone(), &tenant_clone) - .await - .unwrap(); - experiment_ws.refetch(); - }) - } - > + <ExperimentConcludeForm + experiment=experiment_cf + handle_submit=move || { combined_resource.refetch() } + /> - {format!("Variant-{i}")} - </button> - } - } - }; - view_arr.push(view); - } - view_arr - }} + </Modal> + <Modal + id="experiment_edit_form_modal".to_string() + classnames="w-12/12 max-w-5xl".to_string() + handle_close=move || { close_modal("experiment_edit_form_modal") } + > + + <ExperimentForm + edit=true + id=experiment.id + name=experiment_ef.name + context=extract_conditions(&experiment_ef.context) + .unwrap_or(vec![]) + variants=experiment_ef.variants + default_config=default_config + dimensions=dimensions + handle_submit=move || { combined_resource.refetch() } + /> - </form> - </div> - </dialog> - <dialog id="ramp_exp_modal" class="modal"> - <div class="modal-box"> - <form method="dialog"> - <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"> - <i class="ri-close-line"></i> - </button> - </form> - <h3 class="font-bold text-lg">Ramp up with release</h3> - <p class="py-4">Increase the traffic being redirected to the variants</p> - <form method="dialog" on:submit=on_submit> - <p>{move || traffic.get()}</p> - <input - type="range" - min="0" - max="100" - node_ref=input_element - value=move || experiment_rs.get().traffic_percentage - class="range" - on:input=move |et| { - let t = event_target_value(&et).parse::<u8>().unwrap(); - log!("traffic value:{t}"); - set_traffic.set(t); - } - /> + </Modal> + } + .into_view() + } + None => view! { <h1>Error fetching experiment</h1> }.into_view(), + } + }} - <button class="btn btn-block text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"> - Set - </button> - </form> - </div> - </dialog> - }.into_view(), - ExperimentStatusType::CONCLUDED => view! {}.into_view(), + </Transition> } } diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index 700aff9bf..c1df77330 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -98,6 +98,32 @@ pub enum VariantType { EXPERIMENTAL, } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option<String>, + pub override_id: Option<String>, + pub overrides: Map<String, Value>, +} + +pub type Variants = Vec<Variant>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Experiment { + pub(crate) variants: Variants, + pub(crate) name: String, + pub(crate) id: String, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, + pub(crate) override_keys: Value, + pub(crate) created_by: String, + pub(crate) created_at: DateTime<Utc>, + pub(crate) last_modified: DateTime<Utc>, + pub(crate) chosen_variant: Option<String>, +} + /*************************** Context-Override types ********************************/ #[derive(Serialize, Deserialize, Clone, Debug)] @@ -118,15 +144,6 @@ pub struct DefaultConfig { pub schema: Value, } -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct Variant { - pub id: String, - pub variant_type: VariantType, - pub context_id: Option<String>, - pub override_id: Option<String>, - pub overrides: Map<String, Value>, -} - #[derive(Deserialize, Serialize, Clone)] pub struct Context { pub id: String, diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index b1ae71421..04a978bd7 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -79,3 +79,78 @@ pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { } } } + +pub fn get_variable_name_and_value( + operands: &Vec<Value>, +) -> Result<(&str, String), String> { + let (obj_pos, variable_obj) = operands + .iter() + .enumerate() + .find(|(_, operand)| { + operand.is_object() + && operand + .as_object() + .expect("unable to parse operands as object") + .get("var") + .is_some() + }) + .ok_or(" failed to get variable name from operands list".to_string())?; + + let variable_name = variable_obj + .as_object() + .map_or(None, |obj| obj.get("var")) + .map_or(None, |value| value.as_str()) + .ok_or(" failed to get variable name from operands list".to_string())?; + + let variable_value = operands + .into_iter() + .enumerate() + .filter(|(idx, _)| *idx != obj_pos) + .map(|(_, val)| val.to_string().replace("\"", "")) + .collect::<Vec<String>>() + .join(","); + + Ok((variable_name, variable_value)) +} + +pub fn extract_conditions( + context_json: &Value, +) -> Result<Vec<(String, String, String)>, String> { + // Assuming max 2-level nesting in context json logic + let context = context_json.as_object().ok_or( + "An error occurred while extracting dimensions: context not a valid JSON object" + .to_string(), + )?; + + let conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or("An error occurred while extracting dimensions: failed parsing conditions as an array".to_string())? + .clone(), + None => vec![context_json.clone()], + }; + + let mut condition_tuples = Vec::new(); + for condition in &conditions { + let condition_obj = condition + .as_object() + .ok_or("failed to parse condition as an object".to_string())?; + let operators = condition_obj.keys(); + + for operator in operators { + let operands = condition_obj[operator] + .as_array() + .ok_or("failed to parse operands as an arrays".to_string())?; + + let (variable_name, variable_value) = get_variable_name_and_value(operands)?; + + condition_tuples.push(( + String::from(variable_name), + operator.to_owned(), + variable_value.to_owned(), + )); + } + } + + Ok(condition_tuples) +} diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index c02cf642c..cbc7d6047 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -6,6 +6,12 @@ height: calc(100vh - 370px); } +.disable-click { + pointer-events: none; + cursor: not-allowed; + filter: opacity(50%); +} + .menu li > :not(ul):not(.menu-title):not(details).active { @apply bg-white; @apply text-black; From e1ab466a1f6bfcde3d72217f604f711dc8a20278 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Tue, 23 Jan 2024 22:48:55 +0530 Subject: [PATCH 250/352] feat: added authentication header for frontend apis --- crates/context-aware-config/src/main.rs | 6 +- .../src/middlewares/cookie_to_header.rs | 62 +++++++++++++++++++ .../src/middlewares/mod.rs | 1 + .../src/components/context_form/utils.rs | 1 - .../components/default_config_form/utils.rs | 1 - .../src/components/dimension_form/utils.rs | 1 - .../src/components/experiment_form/utils.rs | 1 - .../pages/ContextOverride/ContextOverride.rs | 1 - 8 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 crates/context-aware-config/src/middlewares/cookie_to_header.rs diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index da607ad1f..6d085f36c 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -5,7 +5,10 @@ mod helpers; mod logger; mod middlewares; -use crate::middlewares::audit_response_header::{AuditHeader, TableName}; +use crate::middlewares::{ + audit_response_header::{AuditHeader, TableName}, + cookie_to_header::CookieToHeader, +}; use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use api::*; use dashboard_auth::{ @@ -162,6 +165,7 @@ async fn main() -> Result<()> { .add(("X-DEPLOYMENT-ID", deployment_id.clone())) .add(("X-POD-ID", pod_identifier.clone())), ) + .wrap(CookieToHeader) .route( "/health", get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), diff --git a/crates/context-aware-config/src/middlewares/cookie_to_header.rs b/crates/context-aware-config/src/middlewares/cookie_to_header.rs new file mode 100644 index 000000000..da4d44a64 --- /dev/null +++ b/crates/context-aware-config/src/middlewares/cookie_to_header.rs @@ -0,0 +1,62 @@ +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::header::{HeaderName, HeaderValue}, + Error, +}; +use futures_util::future::LocalBoxFuture; +use std::future::{ready, Ready}; +use std::rc::Rc; + +pub struct CookieToHeader; + +impl<S, B> Transform<S, ServiceRequest> for CookieToHeader +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type InitError = (); + type Transform = CookieToHeaderMiddleware<S>; + type Future = Ready<Result<Self::Transform, Self::InitError>>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(CookieToHeaderMiddleware { + service: Rc::new(service), + // other fields if required + })) + } +} + +pub struct CookieToHeaderMiddleware<S> { + service: Rc<S>, +} + +impl<S, B> Service<ServiceRequest> for CookieToHeaderMiddleware<S> +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; + + forward_ready!(service); + + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); + Box::pin(async move { + if let Some(cookie) = req.cookie("token") { + let token_value = cookie.value().to_string(); + req.headers_mut().insert( + HeaderName::from_static("authorization"), + HeaderValue::from_str(&format!("Bearer {}", token_value)) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), + ); + } + srv.call(req).await + }) + } +} diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/context-aware-config/src/middlewares/mod.rs index f75575139..4533bf242 100644 --- a/crates/context-aware-config/src/middlewares/mod.rs +++ b/crates/context-aware-config/src/middlewares/mod.rs @@ -1,4 +1,5 @@ pub mod audit_response_header; +pub mod cookie_to_header; use actix_web::{dev::RequestHead, http::header::HeaderValue}; pub fn cors() -> actix_cors::Cors { diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 621e96632..f8b300374 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -78,7 +78,6 @@ pub async fn create_context( let response = client .put(url) .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") .json(&request_payload) .send() .await diff --git a/crates/frontend/src/components/default_config_form/utils.rs b/crates/frontend/src/components/default_config_form/utils.rs index a061ea84a..fdefab3eb 100644 --- a/crates/frontend/src/components/default_config_form/utils.rs +++ b/crates/frontend/src/components/default_config_form/utils.rs @@ -14,7 +14,6 @@ pub async fn create_default_config( let response = client .put(url) .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") .json(&payload) .send() .await diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs index 6ada5a366..c17cffe9a 100644 --- a/crates/frontend/src/components/dimension_form/utils.rs +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -13,7 +13,6 @@ pub async fn create_dimension( let response = client .put(url) .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") .json(&payload) .send() .await diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index 4e8b92cc0..dfcde03ed 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -26,7 +26,6 @@ pub async fn create_experiment( let response = client .post(url) .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") .json(&request_payload) .send() .await diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index ac8a45fbc..d8f3bf0a6 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -139,7 +139,6 @@ pub async fn create_context( let response = client .put(url) .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") .json(&request_payload) .send() .await From d407b5ab5e29268f8353f897d277b18f1de51c4f Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 31 Jan 2024 07:48:37 +0000 Subject: [PATCH 251/352] chore(version): v0.19.0 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56346aa55..4e11dbe8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.19.0 - 2024-01-31 +### Package updates +- context-aware-config bumped to context-aware-config-v0.15.0 +### Global changes +#### Bug Fixes +- refactored experiment page and fixed experiment edit flow - (b153486) - Shubhranshu Sanjeev +#### Features +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman + +- - - + ## v0.18.1 - 2024-01-29 ### Package updates - context-aware-config bumped to context-aware-config-v0.14.3 diff --git a/Cargo.lock b/Cargo.lock index ba843304f..806feb18d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.14.3" +version = "0.15.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 63cdb3531..05fa18faf 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.15.0 - 2024-01-31 +#### Features +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman + +- - - + ## context-aware-config-v0.14.3 - 2024-01-29 #### Bug Fixes - added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 0bb6b0f55..70a71b614 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.14.3" +version = "0.15.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From adad811d1fc1f8915e0ca5715c2269e7d38df846 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Fri, 2 Feb 2024 19:39:51 +0530 Subject: [PATCH 252/352] feat: added bool, i64 and decimal in default config form --- .../default_config_form.rs | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 3e3c7f6b5..b79ed8fc7 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -1,5 +1,6 @@ use leptos::*; -use serde_json::{json, Value}; +use serde_json::{json, Number, Value}; +use std::str::FromStr; use web_sys::MouseEvent; use crate::{components::button::button::Button, utils::parse_string_to_json_value_vec}; @@ -37,7 +38,20 @@ where let f_value = config_value.get(); let f_value = match f_type.as_str() { - "number" => Value::Number(f_value.parse::<i32>().unwrap().into()), + "number" => Value::Number(f_value.parse::<i64>().unwrap().into()), + "decimal" => match f64::from_str(&f_value) { + Ok(num) => match Number::from_f64(num) { + Some(number) => Value::Number(number), + None => Value::String( + "Invalid decimal format or precision issue".to_string(), + ), + }, + Err(_) => Value::String("Invalid decimal format".to_string()), + }, + "boolean" => match bool::from_str(&f_value) { + Ok(boolean) => Value::Bool(boolean), + _ => Value::String("Invalid Boolean".to_string()), + }, _ => Value::String(f_value), }; @@ -47,6 +61,17 @@ where "type": f_type.to_string(), }) } + "decimal" => { + json!({ + "type": "number".to_string(), + }) + } + "boolean" => { + json!({ + "type": "boolean".to_string(), + } + ) + } "enum" => { json!({ "type": "string", @@ -121,6 +146,12 @@ where "number" => { set_config_type.set("number".to_string()); } + "decimal" => { + set_config_type.set("decimal".to_string()); + } + "boolean" => { + set_config_type.set("boolean".to_string()); + } "enum" => { set_config_type.set("enum".to_string()); set_config_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); @@ -148,6 +179,18 @@ where > "Number" </option> + <option + value="decimal" + selected=move || { config_type.get() == "decimal".to_string() } + > + "Decimal" + </option> + <option + value="boolean" + selected=move || { config_type.get() == "boolean".to_string() } + > + "Boolean" + </option> <option value="enum" selected=move || { config_type.get() == "enum".to_string() }> "String (Enum)" </option> @@ -164,7 +207,7 @@ where {move || { view! { - <Show when=move || (config_type.get() == "number")> + <Show when=move || ((config_type.get() == "number") || (config_type.get() == "decimal"))> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Value</span> @@ -183,7 +226,7 @@ where </div> </Show> - <Show when=move || (show_labels.get() && (config_type.get() != "number"))> + <Show when=move || (show_labels.get() && (config_type.get() != "number") && (config_type.get() != "decimal") )> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Value</span> @@ -198,8 +241,8 @@ where set_config_value.set(event_target_value(&ev)); } /> - </div> + <Show when= move || (config_type.get() != "boolean")> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono"> @@ -221,6 +264,7 @@ where </div> </Show> + </Show> } }} From e528e108440cc585f650535476402471bedb8769 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Thu, 8 Feb 2024 16:46:13 +0530 Subject: [PATCH 253/352] docs: () added setup instruction --- docs/setup.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/setup.md diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 000000000..012cd6434 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,78 @@ +# Setup Instructions for Superposition Platform + +This document outlines the setup process for the `Superposition Platform`. + +## Prerequisites + +* Docker installed and running. +* Docker Compose installed. +* [Follow installation instructions at Zero To Nix ↗](https://zero-to-nix.com/start/install) + +# Setup Steps + +## Start docker daemon +```bash + open --background -a Docker + ``` +## Shutdown Local Postgres + +### For HomeBrew: +```bash + # Check if service is running + brew services + # If postgres is running, stop it via brew + brew services stop postgresql@<your_postgres_version> +``` +## Clone the repository +```bash + git clone ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git + cd context-aware-config +``` +## Build And Run +```bash + nix develop + make setup + make run +``` +## Check Installation + +### Check logs + ```bash + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.291Z","value":"starting 5 workers"} + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.292Z","value":"Actix runtime found; starting in Actix runtime"} +``` +### Check /health endpoint +```bash + curl --location 'http://localhost:8080/health' + # Expected Response : "Health is good :D" +``` + +## Creating New Tenants +```bash + make tenant TENANT=<tenant_name> + # Add the tenant in the TENANTS env variable. For example TENANTS=dev,test,<tenant_name> + # Stop the server and run: + make run + ``` +## Additional Information + +### Make Targets +The following targets are available +* `db-init`: Initializes the database. +* `setup`: Sets up the development environment. +* `kill`: Stops all running containers. +* `run`: Runs the application in development mode. +* `ci-test`: Runs unit tests. +* `ci-build`: Builds the Docker image. +* `ci-push`: Pushes the Docker image to a registry. +* `registry-login`: Logs in to a Docker registry. +* `validate-aws-connection`: Validates the AWS connection. +* `validate-psql-connection`: Validates the PostgreSQL connection. + +### Environment Variables +| Variable | Description | Default Value | +|---|---|---| +| `ENABLE_TENANT_AND_SCOPE` | Enables multi-tenancy | `true` | +| `TENANTS` | List of Tenants | `dev,test` | +| `DOCKER_DNS` | DNS server to use within the container | `localhost` | + From f0993c7b14e4b397d0740b12312ca262c81bb01f Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Thu, 8 Feb 2024 16:53:35 +0530 Subject: [PATCH 254/352] feat: client-integration-doc --- docs/client-context-aware-configuration.md | 50 +++++++++++++++++++++ docs/client-experimentation.md | 52 ++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 docs/client-context-aware-configuration.md create mode 100644 docs/client-experimentation.md diff --git a/docs/client-context-aware-configuration.md b/docs/client-context-aware-configuration.md new file mode 100644 index 000000000..e67fc4ade --- /dev/null +++ b/docs/client-context-aware-configuration.md @@ -0,0 +1,50 @@ +# Context Aware Configuration Client Integration + +This provides SDK to interact with ```context-aware-config```.We support cac_client for + 1. Rust + 2. Haskell + + +## Rust + +### Implementation +Below is the rust implementation to instantiate CAC client . + +```rust +use cac_client as cc; + +let tenants: Vec<String> = ["abc", "wer"]; +//You can create a clientFactory +for tenant in tenants { + cc::CLIENT_FACTORY + .create_client( + tenant.to_string(), + update_cac_periodically,//flag for if you want to update cac config periodically + polling_interval,//polling interval in secs, default is 60 + cac_hostname.to_string(),//cac server host + ) + .await + .expect(format!("{}: Failed to acquire cac_client", tenant).as_str()); +}; +//You can extract an individual tenant's client from clientFactory +let tenant = "abc".to_owned(); +let cac_client = cc::CLIENT_FACTORY.get_client(tenant.clone()).map_err(|e| { + log::error!("{}: {}", tenant.clone(), e); + ErrorType::IgnoreError(format!("{}: Failed to get cac client", tenant)) + })?; + + +``` + +### Methods Overview + +1. ```pub async fn start_polling_update(self)``` -> This updates client's config at given interval. +1. ```pub fn get_config(&self) -> Result<Config, String> {}``` -> You can fetch the client's config. +2. ```pub fn get_last_modified<E>(&'static self) -> Result<DateTime<Utc>, String> {}``` -> This provides when the config last modified. +3. ```pub fn eval(&self, query_data: Map<String, Value>,) -> Result<Map<String, Value>, String> {} ``` +-> This helps in evaluating the config based on the given context. + + +## Haskell + + diff --git a/docs/client-experimentation.md b/docs/client-experimentation.md new file mode 100644 index 000000000..c2d8ba34a --- /dev/null +++ b/docs/client-experimentation.md @@ -0,0 +1,52 @@ +# Experimentation Client Integration + +This provides SDK to interact with ```experimentation-platform```.We support superposition_client for + 1. Rust + 2. Haskell + + +## Rust + +### Implementation +Below is the rust implementation to instantiate Experimentation client . + +```rust +use superpostion_client as sp; + +let tenants: Vec<String> = ["abc", "wer"]; +//You can create a clientFactory +for tenant in tenants { + rt::spawn( + sp::CLIENT_FACTORY + .create_client(tenant.to_string(), + poll_frequency,//How frequently you want to update config in secs + hostname.to_string()// superposition hostname + ) + .await + .expect(format!("{}: Failed to acquire superposition_client", tenant).as_str()) + .clone() + .run_polling_updates(), + ); +}; +//You can extract an individual tenant's client from clientFactory +let tenant = "abc".to_owned(); +let sp_client = sp::CLIENT_FACTORY + .get_client(tenant.clone()) + .await + .map_err(|e| { + log::error!("{}: {}", tenant, e); + ErrorType::IgnoreError(format!("{}: Failed to get superposition_client", tenant)) + })?; + + +``` + +### Methods Overview + +1. ```pub async fn run_polling_updates(self: Arc<Self>) {}``` -> You can set the interval for updating config in the client. +2. ```pub async fn get_applicable_variant(&self, context: &Value, toss: i8) -> Vec<String> {}``` -> Provides variantIds for which the given context holds true. +3. ```pub async fn get_satisfied_experiments(&self, context: &Value) -> Experiments {}``` -> Lists all the experiments for the given context. +4. ``` pub async fn get_running_experiments(&self) -> Experiments {}``` -> This lists only the inprogress experiments. + + +## Haskell \ No newline at end of file From e3a338d47018bb3870013251f7bdf6e575873430 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Thu, 8 Feb 2024 17:45:01 +0530 Subject: [PATCH 255/352] docs: context aware config docs --- docs/context-aware-config.md | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/context-aware-config.md diff --git a/docs/context-aware-config.md b/docs/context-aware-config.md new file mode 100644 index 000000000..880466146 --- /dev/null +++ b/docs/context-aware-config.md @@ -0,0 +1,104 @@ +# Context Aware Config +--- + +Context Aware Config (abbreviated as CAC) is the foundational service of the Superposition Platform that is configuration management system that can override config values under certain domain contexts. CAC makes it easy to configure your applications and make sure that you can change these configurations without causing issues in your application + +- [Context Aware Config](#context-aware-config) + - [Concepts](#concepts) + - [Default Configs](#default-configs) + - [Dimensions](#dimensions) + - [Context](#context) + - [Overrides](#overrides) + - [How CAC Works](#how-cac-works) + +## Concepts +--- +### Default Configs + +Default Configs are a fundamental concept to CAC. Default Configs are key value pairs that help CAC model the entity/configuration it is trying to manage and serve. Think of default configs as the best assumption we can make about our configs. + +Example: +Using our car classification example, we can establish defaults like: +1. Number of doors - 4 (most cars do have 4 doors) +2. Engine - Internal Combustion +3. Chassis - Hatchback +4. Color - Silver +5. interiors - leather + +Again remember, these are our base assumptions that we provide to CAC about our configuration + +### Dimensions + +Dimensions are typically attributes of your domain which can potentially govern values that a particular configuration takes. + +Example: + +Let us assume we are modelling a system for buying cars, the dimensions you might use are: + 1. Engine horsepower + 2. manufacturer + 3. model + +Contexts build upon Dimensions + +### Context + +A Context is a logical expression built using dimensions as variables. It takes the form: + +`Dimension operator value` + +Operators are: +- IS: an equality operator +- HAS: similar to the IN operator +- BETWEEN (inclusive): an operator that checks if a provided value is between `value` + +When multiple Context are defined together, they are evaluated as AND - if a context is evaluated as false, then the entire rule will not apply. + +Examples: + +- color IS "red" +- manufacturer IS "hyundai" +- chassis HAS "hatchback" + +### Overrides + +Overrides are a subset of the configuration from Default Config typically with different values. Overrides are always associated with Contexts and are applied when a Context is evaluated to `true`. + +Example: + +lets take an example to configure Tesla cars + +``` +[manufacturer IS "Tesla"] +engine = "EV" + +[manufacturer IS "Tesla", model IS "cybertruck"] +chassis = "truck" + +[manufacturer IS "Tesla", model IS "roadster"] +chassis = "sport" + +[manufacturer IS "Tesla", model IS "Y"] +chassis = "sedan" +``` + +## How CAC Works +--- + +- Your application adds the CAC client as a library +- At runtime, your application will provide a context to the client for evaluation. Using our car example from earlier, the context provided by your application would look like: + + ``` + { + manufacturer: "Tesla", + "model": "Y" + } + ``` +- CAC will apply all contexts it has available, and return the final configuration for the application. If no rule applies for a configuration, it's default value will be returned. From our earlier examples (check Default Config and Overrides section) the configuration returned would be + + ``` + engine = "EV" + chassis = "sedan" + number_of_doors = 4 + color = "silver" + interiors = "leather" + ``` \ No newline at end of file From 2b547929707a982a17381514fa444664fc5ecd42 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 8 Feb 2024 18:39:03 +0530 Subject: [PATCH 256/352] chore: experimentation docs first cut --- docs/experiment-context-example.png | Bin 0 -> 35534 bytes docs/experiment-variant-example.png | Bin 0 -> 46792 bytes docs/experimentation.md | 27 +++++++++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 docs/experiment-context-example.png create mode 100644 docs/experiment-variant-example.png create mode 100644 docs/experimentation.md diff --git a/docs/experiment-context-example.png b/docs/experiment-context-example.png new file mode 100644 index 0000000000000000000000000000000000000000..a6aef6bd3d05f5c3433e68fc9dfa4cebc2eb72b6 GIT binary patch literal 35534 zcmbSzhg*;B|9&1@2vKNB6k3G#ctkrz(%xHpXfG=v6)mMjNs{)K%4n!mlJ+jCq`kF% z=jHi)|AC+5a6BG*zwi6LuGcuv^L1YL{Ywf`JGLF%MnOTbLq__75(ULSG!zu;tha2! z-*k3e?!$k!+DfZCP*ChFCBN5&vhSp$pg2S!bK#t_i_Y&(SC_qxF$<H<i9*9KsO^J= z9&Ol@x-0eH{DC030;&S#y$p8W8y&AZnK-GM4o1{>Mzm^wk1*9Uxo~s$g&%v3eQY-j zY*)SUXoG!_UkB}xXoa^+^P5YoBjczA4?a2)+dTcf;cD!7-s|MT<U-Mv6q~C_Ure$N zFyGlFe$T1r#r7@aPae8KYUB@boze#X{R_)|pIzcxnA`l1(BMCJ%=rHQe?aoUuDFyh zCZ}`fGp{IU8l;Hi?#;SX{<|@U&zifbEnmrEwv8u+!{WF}a?Vs(Rog+|rs|M3rRI;1 zU6oJl>rvCz3|sVi6&BW@W0zkjFOXuSnfj((DIzn*>PtP3@td5?u<{y{(<efIrT&>? zw5s5oZe^!ts0a*F>v-!(8DBp%QEsr9aVoN;UH^E6)yRpFV~Lz}wib>d{b9qoU*e;h zbt0w&LX53Em5Xzkrp%r{Y!_|Y85Uk?^fvj+9-C{T1Hzmlf%X~)vZCvzbdL2`{mC_+ zf6$t#sw!Z$)Y4|UzB?t&$3B84pfuDmq|Syek8h`QnxfqA#th+D<9BDPx^KES=3T1Y z>@1olk`j7zWy-zmcj)bv{Gvt6Zngp8DnYCJt(lyvwTV%x>dj6?9H$1Jah#5<;TI54 zPmGST<_l<7V1M$QBTM+i6DztbPvs^HD-HD$PRG$50pByeXL)Axsa0vX1%}ia+jM-i z<c)7MuQ$(eE*<9!ZLPPlql>69Il<0;{P^+bFJErD{ozKX(MKbm%gV}kbkd~a>pP~U zN-c{W5{+8dwMOQ8W~-VA#g1kt6lbOu;XX%q%>8QdOHy`b6AlYMYJL1lK)YXSruJRq z^;bj_k2W1_cMYM-;h8(nXm#9h?w3Z>IrWzhk8d{aaOw*Cnb)XMVB9uUoUNQnpC`{= zF<NGsqnsMm-StjS%$nOoQEpb_LAz2@K{IblkbBLbSwWE-r>E3!|Hv9=??leqnaetM z1JB-=F1aUoq&97;wmH?Z7HydlGpeV~)IOU@dqsi0n&bA=*<*tlop(d~Q_9A!b}8*K zl(`pDsUX*170%P8AusT%(HsF}p7SS_r`?rv#PWV?YFfQnX?r^zH#hg0gFDP~4yLI1 z1qM=id3m|GyZ8V8E$?p8T>8=MOi*lWoj)DltK{VVse$S!5%;j=Wsj$kkty6&Jowg; zcPS}%i*|f*(nwHJ=kp4!TCHt$Vsy70%<^v)J<(cz^|cV6hWn{%hgZtZ*1P6@sh4xk z_7wU{s8=|dB?XM$HCuXBZ}!F!VJO$HppatoJ9k8{usAnxu1V@Z*7n@$OFP3{LvBmR z^`~59&)a8Q6lNe6wz+_-YW%KGQGU_>j+NE6a?QwGGr10(?Y}Fm4Cl(NnhO)`aYa2L zl1N=`*_mH>UvgYgXlKt#SA9ueqRP;Iq0~u3y@Ds!zC&F7CB0>Ae9oUR9{1a_j_wr} zOHp$_uLNj@%fBqlrF5TFel0XFGuN`Dqefy&Ia4^c)$(Oh(*2h&S!dit+=>|E>+kvb zQC_}$d1kyjR@C#)*DO=TkDoqGPEEZvj9osTEoV^Q(ju*+!zdsiK+kV`!(p&ipEq*r z_U)w=6`NC3<St#>#mvmSVbi9*kr4}jc0N|hcD{-|d7aWVKe2M{J2VccineLqH5P3T z&(yI9<g7`2m^UMwJ7Oi2`X-qpD>^qXwaP7b#F)3cC?gLM#uqtmJ(`}L_o01>r^8}d z=hRVS26xBN(AIK}wCb*GEA51^KHK@g`^E(+*Q{KvX3vKi+lYj=`zgr{gw2{&3v-r* zG*pEfcg*Fqv#TnwGjU?&H4WxEEa);jn?f?e%KwJ1wNS~Z9-YPU$9a73o$r?v4Ybi& zKI#58-NvOVkUiru|Dat#^B2EB=jwT*C6C<djOgav4yV<yuy~F<zCf!tIfJTAIjVIZ zotDl<)_iYlv>R#SJaOX0F;32eH*X&H^%*?y^E=MY9{=LSofr{!9>-tGef|Bz4T)4; zU0qGa-nwG^8OM$t>-p$I`MawqGFdfk&#qnnUcY|k_NE;M9r<<}Hg8_*3Eew7qI3VA zRYz`dW=z5trEVUzV|@1QcGcOkv)cc(Drvh<w+3h)s}5=46nQe5JyqY8FA`?;E=N$- zDyjWxE=MA#BWGIW+>!-d*1N>Hs^21QegYaM`f2h9uGBVbXjkXkUn=Lc^!^d*7&@67 zmS=oWqdSS+aPFl<^Hn9Kj|0_D7Jo+CNXyBw@bS@ldwc)<67ebI_?0bGRBPvarHh>n z>N`5*Ra9sq`D|E^AHV<p{lAj_2g`o9X5D}IFvMRKea7YUz>gp4-fLcasHrP!YK%YL z*~+1jeXpfO?Ydxe=G*hdss{Dnzn3mf531)`Ga$T<962)a<#gRHYU=nmCrD58)v~we zDRdqu7x?^HS|wFWMqK=#v9YnL*Yh()d;BPAxh8zYmT!LiC?3Y8UG>g&ddrqAmsF_H z<t}WtoBnyxX|%13E!N{!Nr@PD@xtLxzEn2T`j5xmJo#Tedv;?79q%PIwa+LE>iM?M zkL>&VvBBQn4-fXSyY%|d9%E-W9ITDjD)G`*AB|}XQ9E<yOuAJ&Z*7#or|(HuA_N>* zQEsZtNmCtq_Omv+I_Rhzzr)Y%OLL>~nVCxJ30JLTIlg`SwlLRjvz3z45Nmkn?%fl~ zf7|nI&1|#X@sj=q^JAS+0uDa|EWA4WKHc}JH#>sI4GP-B=s9!QZT45fo5N%+6yA4s zMkifK?ECdg7OP@xY+QAM(;jhj{CMxT1UY;!kJ0B{ZyiTo{_p1vmKP?GqYeub`dm8i zhHR64KHV?#p`<;=!C~~fCF6pm<Ox553+nHqL7lSyA&oM6;J|_JO-(0~TLx?m9!EsT zSXpuQ_V&8Cx&7})EAfR5C=mEUX*;`&%oZ+RYCqiSbwLJ1wzF2G7J0Pj;MiEEbLY+( z85>_#Qi|iqt$x1!IciQ@n})c!_{8k&RT@eHP=X%>1eBwNsjJI4IAmomaP2%w<LKx( zH0yTj_U(a__sGrppe!+oiHRY*zK+aR^%e4msHu(V4G#?XBqt|l{zW>`AqkhCzkU05 zWse`q+CxbeM2K0AhH(Z#J%Y!Xn3!;XSTH2&ali4n%bR{DrKCutHTRE=1^D^-WiC87 zJaaH9Ep79o5i4^4VmFeL*=uWSe+&*@(bi7kDE2$gNt2V4bB31}tN%c<{}R_!yt$cK z57s?O$XP*s<`V8rtI)}RX6Cx_<wR9YO*vWFZG1Le$~0S%2hvH(Cr}WQl9Dddbe26Y z6;EvF{r&r)Y=U@#5^ku{h=<e<UzJxPc=S}AB3TWMr<t!Lfb4=RUu@4S{Q1))RlBhK z?hZPB*Qq0%ojE#b%~;~`OIcT>P>YYFl8k$=33Yo+*Q|Ff!JaPLn&&(n>ny|rrgFUX zJAdCNCCuqg$~D|F64Ya^)ig@jHR#J1dEklh_JFdWBm8euFJ8R3ckkXup`nga9hSU8 z*Uc|#Dyyj2&kSA8*5FhWp1jj}p~jLI*R8{iw1v=j`u*2Q%FD}z`f6%wI^MZ9^v;hg zDJ3ay*tqeOZO^~CHr-<r0`=GgFD(THg`MN746+;?ocjVezQ$mql&)NH9WR<+h+SJ` z;<f4ef@kD+{Iz#rV4&)kSa_>tIqt$^libCNJC^3hO-Gtil$Dh)si;UgI-V^kD2U>> z``D)2@5>V*kK48#{&tjxOY2@_WMt+xTcO=@GBUkipP%2!P-m~~=_&GrTeo;*m%Op@ zQ5hMT`GKrUQ?s)JkEIwt)YSN+D6o@65Wd49aD&D5#`*gjJxap1Q36=p-F`qE*%`rO zyiLS??#SuW!JL}8>85otPHTg|%>Fh;R6vx7{;E)R<nUeXLMOYP_v^kjG|0%w8P>&! z?9?rv(K&eV;OFY<PnDJT(O-}G-E1Gx(bAGPFgSeBzx=rgn^b5>NV?~e6VK_><M*C~ zhGsYnDx_)^82AJ5J(P_9lp-q`zq?4sCa)Mkw*TkZ4<9~|N;#U>ovZnc_0F!o)oXGF zy|#+-?ic3*cn<m#tQ@USF;pKvIWcjFiz}eNA~4H&Tu1!;`2(j-wl}BgCT3>t@s~XR z@4pntZhj%5E4sRhUS6X9{{Gmx-DpKuu27#nd)CFpMU8rk+-ZMfDU)lvPe#aNi-Vs& zJ*cIvJvlook)m=%UA=N{v|UC);bnIA-r&j$fC)uzvze)eX*xx=4i5c4Ya*SR6%&*$ z>*@xoBqzkh-TL##?eO8lzn$!@EG=KWe!Y!~Dv*ZTPe)Z%3Plauy0Em=|MRD$gTv|2 z(9pg6_SL6o@}X#G__rGtm}}rBlT}jQ#(xRApw`t87k3x|jKpQ*;R)(0a$lb(R)JzP zH8pi9N%@YC&+dZ<Wf47tb+L)r*&)aW>V5kpu%84%qfMh6d3bo-3k=+B9h*c=M_2ur zMe6hC&w8I9G3O?;U|pGiNGq+glIG|s47-v0ZDba=bNTAkGF-{d(J|re+ow}g_J3wa zd{ONXR=+z6sDaAU(zu^LfBuj`C~V#OO}4!sH;OL)px(Y|eY{kl$<&X^{QP{<63YXb z6vw-Yu3-_<(|6%xzP_80fcbCVenVTuGA1g<zQpn%2>bf_?)m$dm6iDn4Vf*ktQ2^y z2x4=VfAW=Nq&g$GxwyE7uD_*e<`x!?`tif))~#F8^722h^vg4eVhM;=-tqTV8Ywi2 zii*}9`RA}AIyySeA$+{NK|w*{SoSQ_I#w2z^`Wv7*RCCp_4;$1O)<vk{f%{qvXj;w z{O0E7V-EtE#2BL$oGa{mox~nIc(4r!dbmEmf?@LY>l5Y{7IqE}hHI-U^ujKWP_0mI z8(v<FcOLI*nzzG(zeq?RXaVrjWp!~-07cBAIkgPn9bx+Z=KA5rWZKO;>7U_w{Utvk zZBy?1$R>QOs|&<Z2Zx2R68<XuV@%F(qet`MuPU>%vmqfNJ%fY3!NJt))~(x4&;Mv* z!WO9Y_=yvJ+}?|nZbdQ8I`011^5F3B&o3`Vs%08&jrE*6fUOr^UFiP-U~;X2mG$D& zQvA#Te%nJ7C_9$zxjSrZY)rm{-~6EW9t)0Oa{AqJ&d%=C`c2z=mKUe#g`6Jdb-DRH zdv*xxE2XW?pzFPI7MuUx`Qkjb2;bnJVOWuASh2HkgwNrpJnEV}(kvk%VPSF6Zh673 z@ILw$JJJrNekX%q=^OQ|H=J~m@s9xlGm-cm9UVyA&Den>e0)!UBzdjcE_4@r0*@P1 z1TdhjZQt}sd(yh|?J1Kl`^!#udm(HtHf6bUU>6EpCJ&?cE&Ta2HSk0?C^EA8;epfP zrwljk-o2YbSyeU5qM56B^^XOk`|ne~e*NP2UKMtAbtQ$1`TX4pz&#~x?TZKqtkR2@ zFL}(q9-`sakuULDc~iHwr?2nUlP9!!xH1bDlB4&BnSp+Zh|Y&^mTR8We&ywoD0AUw zEoj8n7VFe=Ee`~<DYCM%mbYCW9~h7TZ2NR~2g}KmkBU8);#86=zkK<DnhDyFY2J9k zq&7;%-2BAi;^O;J$K*F}e#{IvL<u@>L&VCnQg6DwF1}tGg;q6Hs~68@f&@ipB9Eu4 z>uD?^^s%yXD;padk_>HVFD<PDvhL~Ar}=jMn}8}gQ*|$2-i^&c`=4yHNIPX*wKp>> zD<!QnOe2N$#EA!|49n9|9+i}o-l(3fsjD+>PStL*u%Q>ce(%<;e-xtlKjc_6UrKI# z$Rrjc;YYLkz=29U#OK=DVs>kpfRL!@=*hV`!l~Z8;lMpyy?QlD)br`|VBP6!-;Tt^ z$Ez3EAH9;Oa1`w$t@zIaRAWslZ+a285PXaMXxrIT-ICfnTd7em<6pkKySlQ}fSQM3 zju3L@VpmNysth`cU?Ka4fVp68ojQBzDv}saWWVk-kS7HaLqe10wb7Y_=<t{BrU(lQ zlNiF*l6=(6vwj>MebLP9*yG2K(Hltc7XSIhZs8}Tr+28xecl9x=#H<i!`yFmz(<qO z_0Lt33$S*`to{Cy3bCRW(Yp~=@=wp)r)E<i1QAf*2dt5ikufAJ%zmsxfJ-a?U)&P_ zd1-U=lb0`F(xA<hc+LE74K}V0d;I9pzX&VD?<cb}*!@uMeP9$ObukyQGidCF1684@ z*OMe@gM&Y1Uu)PxNx9xSHf0YDjpX_B>v0{eBDYYiTzvBgn%cA6+__X2Y3X}?2W+k+ zH(t1KVHX2KjW3gzG~x`2{NlxnUAuRGKyINCG-dHh2GCa`EnD*KSZ;5oV&&l2PD@MM z<<h@{ph~kdGb7D`zkgfKxUDcGrV9!P^p1_yK09}N{~5DgrnOP}1fL5z2PDS0ZM*&9 zU$kur2?-9(+%G1aq%1w;;Oq>T$x~kY(_FHe!}Cg6M@K<PX%8~4sd%fE{-QJe-n|#( z<sWA5=i}phS5!nIA0P}Y?>6>7)3#R}c_Xc$U^3ZPM#>8s<V(fa+J(t}aBPwzKs_h* zKW-pw06DuKwF>nQ8*_IjgDf@%-Hi=t6}$X{?cx4Yq`!i2pkX9n-w`%s`;a<2cI+T^ z5A7YuQy$&<-u?R-g<h+xt9S0)nfS~SsMbw;`vYqJ`roWHCqhZ)xy=lH05=nGpW`Sl zF4iir_Zf`!j@d{_a}ssI!_)IP7nfvwy_%5U+rmPJ$vz3Rb(fiXspA|R@7P7XS3MAg zA&Qh8Ua_7Y9{IMt|6nO0T;07KFPY)7Xpb+rW^Mfw&*$-HIFTUh@$tY9dOaXxNPge% z-<8qz$JE5Cml~5*xsgRFbyqK4dSC8OcbuEM0<8l(hzFgRnaM5=M6)r(g6_0>MX;u@ z?@op|FmLR5SG{5#V`%G5)_XwaUMnNIr|tSKgmLS3^#vp!4ZE`Y?7@qXHU>^VzZ_Vd z_g;HavbHh~IHXCXnGNuXSVKOgkDa8WqpKgj-V_~f9L<qI(g|%7o$|cz&Z0rO`SS<- zB>_34Wn~S9zCI@aYX5#S&b;fAoM2|?89-6#24&koOF$`5owA^y6!ry}nVXA)g8cmH z)ZEJ6*w{!f>Jg5T2~d1HF_Dc$D!`~EUEc%kQmfb_!mhvkdBbqN)968LBq7m5LqkHj ze%S2V7?J7^w>KZ><Rl0oKcAmfF-G3dkX#FCA*}>D!|=Xjb)>|8b=lQxHd*(S({DB7 zNd|hsy#~CIDMA#Rrydo)YWy|sDY7X?!yh26xR?=q%Q!<DVStdeSldxu;-y*TXz%Q7 zIx|$ydv=Ul&fD+Rt7FEyZI`wO;SZ=9k|^H@OXnk35BhJSrG0`2v~_Z_KD8C4(WL90 z>z7Er@qqkROOwT^0guTtTCIHB4{l}zf<O0OTXjDAP%t_G4|xgzMW~OkwwT=OzP86Q zZ0+qql03(2BgO6{EQ+$TSNE32oz8f-3ZO$oysb)dlS>i_Z#@OZ5S=>eM?fnnZO>%b zF4&|d@RhJoS}7uUOUue~+8eor4&r&@;^Oe7Z{u$p$=MMST9h-8o^#&Yf$2Jrel;LR z#A(yG=8;ed?OW&$tgLrK*;Tvj?oWl0ySm_RF@K(!MoiMrN5gYTB}F@;u&}T*>zc*` z!e>X5I6Ilix>hRD*QkKAZ>e{ErJOfzKeokp52cyrX+mh31!<+`*Fbbx8%8zth1$0N zCHJ-Qoc@&fOWVQTUHB&P>kMI~A9v#~!os!%SN@ndMf3M>cUl!lzXY4JlJjAq{3}-s z){ps$TLy2FqWLSB)W&yG`AA!!E^T3^uUy|2rN*Cm>F-Aw=X1(F)^GgFyB+emJvqMo zWEx~O!q44Vu<LxnZgVU9zo~&EuA1vB{`dCJ3IuJ=KK_?RUubR^^W@-|-)49HIr-3# zF#V|Rx}B^vxzT*&a`icCQxsBrw&O`Dul@bR+n6Q$v|ZKl|97Xo(sDkKAbgJO#pf?H zAL0CO7@-<4ws!QtOC*m~Ki}0Z_4j^70)DK6coNe8_hQRqBb^ujyG%#kdFCpzb6BK+ z9~3*;XujE9Avg52t*Gpyc+lS`94WvPw%MDaJ>usW=V$*>x4O1-H@Wa&&(LajZ>jhn zbwc&E8$1Q}k}cR`$BoOqDEoUkx3;3djr?Ag668ms16gD?@`tOn<SO&O)%*K}BDiX9 z;NKq=Y_tC!ck#ZD{a@un+m}V)-^e|bQZ&c#o%d=bG&sn2PsSsZ(;vO~zb`L?aIao* zw18~W+b}0Z4vr&9fB(!SHAR6H95(!W8ST+717=enaua`pf#$!tjdq=<MaHE7Jm}C< zBi*k#*#oRb2aVq|{(X=D3>?nz@ilaGJp7>d44?#X9Q-{dCI({n0LN&pC2!uau8Aht zRqycdeU;>m^h;6}ypaWtBcw^7uMj*lH6<QzXQp~YaEq_##cLTuEd=5t6(L;w_;HCP zw6P|VucX;S{JQbJefx;;f%b`N{JE+sE<ZnfH;d#%-P&>;WK4F`YpNz$5hKlM{ZMQY z69+vPS67!x)`Tc&IP`veeoTRK+uzss&HS6PwswsalV>T4SyFN`3mDxKU9SlIAGG{8 zZ@2`H`BGDc${I5WJ_Xc-bnWBg0~CKrLqiq-VP$FF^!uyJO%IpOoq{0S)buSyGjD0i zM^(`2_kNIpYYi_^KSRnm1p#?00+|Mr5@X^}$*T;QFHjPZkYESkbTA>4`Z$T9>w=*0 zAbD3+Y-m}7w`R3I1k=Fsyqj&(+)l^)a38x$&&Wvfo7JLlc~*4gll!+5WcA|3k=L(Z zQxL33FA&tz(?b*N2jReWqUT(U$CS*UxzXy_)fu*2t9D7iG05gszxtTJg?W}Br+r$N zJ4YLY9)yJK-N_^>4={yauar*!07FOuOMC>3E<8~h2jLQ{f7*4*Xw!~^*Sk;8RzKl( znEiEi(@y$(D#>M*ywvpc9lxe^O1z>SN1E)=nQ1uG4uL{heT|o*+C;_pg+1+^xQk2v ztgW<%hcN0jh&wt4MkuDHrkJvztN)=hmf88;{47vk)A@~HCs27&73<B;Knv`HcBPY+ zh>iaB%lxhLcmVkC_`B(yfbU?p$C}2h!_MzTGk~lJo^ul|B`ImEzvOLvrl8<Jkxt#y zvljW+hxe$ftAiNYt^9GMAS(kRxKFP53J{!#+sq+R(HO#UTqgAF?d`|%x5T@+2)tc$ zMn|}=PZbZcG&Nbl6#VGX`_GSA?%%)v0hJq(4wa4J&$mu0L}iqicZ}D{pPaY1*NP9) z(GeI!Vihpv;lu4ssoD_!C;@-osAb#&1<VWgL9q`C3%gryMnt=@4?+8Xx93q|Y24fd zp-Uh!z=gg*@e{EBvBlcj8auoP>jm&m;GSUg&QRGhMC;bATlL2}3ea(S`})Lz&**q9 zdmt8HAMc88*7c4?hn^enu3H)}QG~b((NuW8!$CfZzuxU0BvM40Ji?hQ`-KmmKCOpH z1I<b^*HRx85UG-7KOp;3KH_6#Fxzl<i5PTd79pWXptSzM!3uCaewT?ufg*GJkzEw; ztE=TExjIs8x{GH<+i58P?^?4=@B8_Es;Rkn`HHY*n|<lW=cWG$npkZ77Z}&h*?Dkr zFxGUU=fn5r<}1p|7eH@2-#W%awdv{6!_fEW(W4-2UG<M&rKSde8-t@%*4Dl(D2RYK z-_hAQ?h@?x-~qo~-v)@S1o{Jk_4W3iLvU@TVmu-y#^mhm48cT7ON*ZS-NZjYJyx<r zJrKZ0{?mR=n}0tB0pra8^g_rjL>EN@fFclT^zEAx5+BG0V)vf?`(5e=M?rG}g{QVi z1<+q6yGCT4jt(t*2ZsY<Uc&4uZw~SD65XT6$@>n%Lv*2M{jZS`&|4x761dDL;x?hX zHmm!7v^&kY+w+O;>eLf}oZH=yS6p0OC(G$<(!EyP0vLoy#zyj58DMicwBLq+nL|Bk z$+4h?+CxTXpo)*U9=-iPF91TWHP42rI*hX)m-_MZ=M?Dm9(wvO*!em@TGz3HL9$Z7 zss=spZ@!!FaxX0}4+;&HaB|`WRW_Xc)tC=p-Cw>#`9!E=i5^eHSiv+=aGO#zQQ(84 zqKHhSD7yF}`<@dIfeNBDVUq!+_Tel70uiVp+t7$DEldB-kq>~%5Jd*dLohDTSm4d| zn~~9E<)L4mM`0fwt(okvIJJK}qUDL}^iS~I`x+@fv0ra=OJXo$fi%Hy+e2|8^Z{sK z5Ofv%)8xzyMgqRzeV3G#OF^36r0n}o>>%R<D7EMBZf9m;xeaPe2n>P*#oZ9Mg7jnr z8mp?RI>yEIGr}~cw=Ks4s?OI$MJ8VB4rv*gt-#5~p!(NGn*2crpo>>c*NG`!xNztE ziXf6e9GL-?%zD-r%{vd06stni=Z*r06wl2Zp2@h9os$!U1py*;6U~Nfb$j<}7&4Hk zG7!!nB^nHWOIYru^6qK3DM`r4cwAK_Yu)v(5>JNlogw%TDeEY6;2pkD_Au_jE@c{5 z)6_-_`KlxjFs&_Y1|9q{FmM^X52_`!gLqU%L<95$v;|CBZe!pa#;JK1l9t{{(hIsg z=e2owc#d&%OZ_*5KyyLSeXEFB&bj2qzJY;8113KVtZt#uU%7PYU#ue}JEGG7Z6oV6 zqZF4H+Je^eN2#@lIvqh>1Jfg2#mMLgisv)MSkZ-rg&x#t48Bm;b2KhWN^bIAYVQu` zvtfi7&Wf2HK)$eu$l^kOAo4+f?sqFBALu_d7HLIyF-RwKy;G+igYWG^)RK~IKd_CA ztEPX}KrP5Jt3Ql@fL{6x`M-YiPQt)Zi&IuO$@YOBrM;Wl#^gD(n_Ny%@F~bHiJ8P$ zud0>|Lm6r5p8i0wpWu2Vo8+EwJq{0-wzWOk+S&?^M9suhce+lPFroHbD;93<w3=v% zk&zMjA%KDEB~dw!u(3Hn^#5+2Gkd)Ots>+4uo8H_3?3BH0~<eoI7X772&W?C5mwLY z&GOx5Pe2lBVpu@+RL?N*$=OIU4<JQz=@<y80Z+smAycf1LbRX+c5>s!jisffdMEe4 zf*|Ewvcw?d^t%UfK$J0q(k&)6PbHvU=<DmxjCDpriq_K7a#<XBf+tH~y@u%!#ryX5 zNGQb67${n@uTdit^s7RS6H&dJ5@wdqNaU?Mb{vHQm7RU&-Me=!MeckN5fRJnB`b7j zoKx=8kIbuAOD^7*kb>MLdEvr_D=+2szdk!hMq*f!Lf)>gUoR10vDz=TdK(!Ec>(lg zXMeeWKh`+p$rEa7>QBEKlR?#g*6?-zsFz}b>Ka*EdI4k|V66}B41uzXmR8EpFn%`Q z2zVehHFZ0KV94FOn^vdA)`C&z$P@&shiaP6z?X>A-@kwFK|-vs*)C2QLylgYX;6ex zfmOMSD6mDrhI|nKjS~ofh&EUm9k=157L+~-2{Q5ZUpqQ1SLm}cGcl=s|LIeI>ih>4 z^aqWU+>4d>cKw52X4ckFz8^?2y57OK^BdGAgelRNQKvi>dVM*x@*m+TGhL@mP^?QP znlx=NUZ}gZk&=K4C~PNSW_X9;HKeJan3&pEm*a=JOG<L6-@-6bQ8Ac0ehvD^5Qgz+ z|3o@Ajr9yey#z9nmzUp8%_<Lhi3USMP;1KfFJyv=h7=Hw3D^eItxumm-CnnT6Pdnf zJn1fUwjuI9`pwPu%I}!|Y6wrur>LBUG&?ueN%<aYPo~7E5KY~m9zEJs@KS=U=PR38 zq}uiDBdEsPLAkM1=zT8|6ETIMB9liVl%j1xYXU;Rb^zv{1ZfA_yMq#hux!$)#+Ves zjoyF5KWHZCRT+?yphLV$OEblsnE+4}vk?hB4k6o6Ss#VOPY~u%M<I+-@Vm_%BSC_E zMQ#w`H`BC^5%Z_5+qNAR6YKuduXOqHIRryPqGAO`WGJo>Hj$(Qh@hi~4&6YvqPqQo z;)X(Ed~tEtpEMKPGlzQSo$&B*m6Fmhoe}YSx#~IQ`_TT*CpX5`n~ll#czSxeuFN(g zKb`@3zgzt?(!_(21H!saY)x1&x>j%veu%}+aW52BV~k3O#*3l=SYGm{F>U`zz4r{= z%{oPHlYM^N|D3dJIZ0t)XqYlU`acoa>BYRFN>)Z~zQl?#0XG4Zfg&vT(Uv@OpC5Za zS{p%x>6B?jsF;{pjEL&Pj{qRi724C{hWk=4aytvq(bV)cJbZuC8+Aj^l)hf0LaIVq zU(vFFF!>g9Yim}3Enw>2!9gOOLX|{^?jISEL$v@l!T>1Preq}yv-}>I2+&MhAvltN zM1e-z_}0?$89d{jLL$qNBUq~a(B}LrjSgc%hq;{Ek~Ppizz{(8{_S@QyFLL_VN2Fc zOibuO+!CHFrACzpmj4$U1CbfiUOxN<aM{h9H-+a~49}cB+Y6Ah@{jMGJ0~%FK!w7# zvp~bZuoWWX$w*ST9gv}z+4(|i>nro)&@En$p_4v;{u6`D<6R$5ndh{P^-}nu!Jjy< zqLC5?uGQnjrs#L)&IY2Fk})mjD{{-r%g8A)gvZ)B(<d7@Y?y$<VH&|PSt~T&gZUjY zOf6k+9Wt!ymkP8Um@t$M6tlT=9t=}RiE{uQ9{P+KSRleu@8qsMd&Ch2(3{D6VknPc z7@P=4jvhS)mIK)Ru&_`Ff-2_mnt_2k8HK|E?4fBxts<rp<S;52EfNmlrWuZW>nwCm zL_La%qQ`tNg4gP4XV}e!1s76BAY!#<8WY8M_wJim9WJh&DJs=S1hf_ce9&P3H?Tt$ zL01TR@<d8spBb{T3H%f&md})uCZ?zRhZAFSSJP(!a-W~~wg2&XH%5srefQbue;wOL zN9R{kvgTR%EO1w!H8uyk_vxut%RxX=NZ6`(z0)zt>vA1rFjl)a4nIH~3)8j(B5r3e ziQbH{Q`uO_IC_^#ibffk;m*ulNKiWE`RBSt0Ys<6ypZ6H${!wZK4XQ+dnS}cEC_*O zkSGxW`X~1fVVs7!!I3j(W>QNJpE(nPE<pjY-O4|3|EZ6HW8>p>@qwb--aFKZQ~)%z zyme$pD2LaW0JD-h`)6i&!-fqV^~z^WYw1Vqq2rRNU1ePz8wbZjth{<%*tHrASIjIe zmoHOOqE3M*U?PbD)0Hb%&SQ35TbswtWa*l{9Xtt@2~p`(S1NPQw;oV<aYT2m3b<w~ zoH{g=lkw?!<CZd!d;9{zl;4k92z6Mn{lX1`%Y^8&%Q0=tYV}Zch?k7l2QA00J$n53 z9FQCC7v=A3nywgz+U>@h`5=%(Q7%9OKxTl20<*|ttT8z~T{!3~3c(uH6IO~}=Xe#H z8XGYP`G6KetU5phxbKFR7MF_SR>*&l*5^u{#kXD`wH{s{uhc0T*rik#wD)a({<B7N zL{CLS12u-SxGW4kWS({72F1ue%JH0{Ep}pM<>gaohnh>gTk7i}oDIDOSQD`Axsh}F zMG%4&U=V|074`4d$|oA<Wp^@4f?A_*!yqMql8j*$xfe`Iv4#MIPtTwtkYW48#QU0> z9VnKrt^#DZeBkt53~?aTq*UG=$ov#WIf>cUWw=hD8o;N5#vdFUY&hzT!V4{;$*QAu zajuhwg@t9fIgJTgNg_rwn76FF;^d1Pff|~bo4IW=J-<0tRWnG9Uk@@if&9QOSX0f> zS&p%n%_Av%XYln$f#uQt<ynJs&OQh5g981}`t@Xhir_T`Ho368=EasvH;blP-uub- zfPnoG1nfJD+kBBWx<{M3^p4w@nLWce9=KrJwr!s)E15Yt{lVw$p#vaWiEjdsZ~yj9 z$W}5KCeqHrMDO+kJR6vpVufPhEGwOt=6Gumw0qyaG5{FO(E-k}Ff@8FnuTA<X|GaJ z5`b0U0;OhPuo`vw1!|1AyB`>63Pl|i9|Ls*E1C4{3oRP&Tuvbtn%;K%qU4}hVG;z& zf2ZCodncNQk&)4(m>2~Oje}3PwEF=4kYATEgn-f<fQkgFPex*R%DnbY7N2w&Bv6kn zD766B(Vv@CMC9b62b9|WD&n-*M<my$&Q1n|Phn!=wn<Ncp!En6TcexNk0C}Vip++1 zoMW&ZsM_Z=)=>%U1OM5l?(SHSZzy=Ug^!Jmp<pj4a^(}fpN3nr*nke4kL;N@5D=O7 z^`HqW-anx|s6|OdMTLP$1m?a7FH~@J5%i?(AWJ{dVbSqz;R%BZ5r~H-+xG2+%)^6c zw*mT&WZubtf=_@X!&Z^_{P|66FgQ0JCnPjfU5GOgf(;ZUSOUn?UKd0Rf-%Cjpi##& zJvYaSgh)?6X<%T0!OR7aN=S?)CEm$TI%wjKS}hhi9?LMR{zr)2IPs!gA`}E7g%Cyn zLX3LwUhgFAM?Oe)Q1EwiZ5Bkg0DHGMJn8A(6Jpw$%_di&SnYu8V)#`Bw&{Y|5m5_8 zM4~YeC|>+YM~qVNaqxRAoP<y2OO!w`M2`jtUu8iOs-`FR=eth7bR=eAY$ut)WoGio z%E@7Tl7NX3{s%Y*S`M~|WLM%d4Z%Z&`T1&TJtKpIrWa*_<%xZ6ZFL0+*9#|}iwpnc z<RrOIEIQyfibCPY-3Q{!mv*iQM%FOQvl{iSR#jL31+SQUC||uwyfCr}aWF^7M+=7H zUia+XdtK^q{ci74juC5tghohN_}eL<95NjaU=*o>a71RXFijW(f(lQ4X5r9z7Y-xk z@ItG^f$f`KCnqPpdGi8}6g&Y@q~Mc5F)=bRQM!87Co+-_4Y8uS8m=HI+%UKW)U1(> zb1?jYzL=dk2)i9w{_1K!0K^#2Id!-BF;A&D9aM9uUu41v;Dd5NIt@Aoww|av5LMBX zZ-OgA$^7<|xy^^jm`C<~>?^%bZQ1oM0z+^3WqjZo_@DU&(&=^#ezCL+qVMV-s|A=^ zoH>Zt261?SF_7@m?=0k;?a)_d5*W0)9v`3qS3($6FZK`s&ilMx#7A|R9}pJYnFx@m zosgX&7BGv5Fcj$oK6&zFR)d{TU@R|e5M-!``-99zOyTe$z`lUUd;tL3^x{+fcrfk_ zX$+%_njx4cI3@f|vggjNTU)3QL#VLA#Yo2f$T~2SA?SorPMpPjkm%*a>vykRdocGu z3sftFAka=JV1JJVTOhA8WGH4|xvbRM*EBP%YW54eoXp6`c%xJF6p=~<2n@t+iWgs= zT?{{|f9v{&u{+2!d_~^aJ1XeESFi5F;0k!Cp~TypbepYPnU5X2hq8})3%*s_(9qC( zr`Px1g9nFCoTw7?>?_?$;-BO?L_j>?QFQABxhG_V4(STuYJT=>{LucksmQUfP2U=Y zY@Cc?mxzKPUQui%1V#E18VF$6+7$QNN6?;$xw-PPvV`iR)SMYd4gd=R(4K}(G&3`^ zrPx!57+-AOO>Xl1Gouf!3KHh!osX~o6V@J-_~J!63^4GB-h=#r&@%;c+J5lNJ+K4c z%zb(g>Pfk|fmqu~&;KtbhfE$5>o6twZve>eHuqb`$mqq)*^xf_6@Qy62G8kfqg5p& zHbUpM?0g%FW&sbufl~$>>O_|~?Ck8Cu81>qetmnmx;F^aj0h2w-0$AQc)jpDt8SsS z&Px&&7MFog)%|?dQS{AY&mfHzIE_A?WxWeQM?Qkb2Z{jMx5B*B-fBDcpTrn^69w^v zK;k2$68^(P#3x~%KyDa}Pkucr-S^=XS{n7MobFX#HkvqxYGgSaf0*t2Lhu1<B3g@F z7zZ;C&z=+&cA*cvP_ju0#~#MG{yY%Iq5ct_q*iQol1j&QU>_XfuH+sCufMWanSAs* zEo~5z7?}XWc29va$SNQH9vaa8QwA4&ZSK1*Pgc0%cO@KaxGX|<8W^0KmbL<S0CBiv z);v39RPRFbk4py3f+f&Six>JRMVEe`21%TNn+VDmnIr@7Lb(w9B9`VGDw~km#W9N6 zh{5XeqQ_Jv8_8lqkFh?}9S(K%09*ra`k0lpw7!5Ms1-VKqQhX^bJC_u7@m;EUg~WL z@Hi4i85A8|1BF8Xj!uvwUead(EQz!Lt5(ly%s*US9#%4k7`pbfWnQYcBi6#SlGUg1 zWZPnx0Xaks%^A!0_)edwy^~WpR7T=)AVkLlHUkexEyQMXHCg49^OzP&Z)nziCUvMk zKo9*8&kn&2huJL;JUGN|Zl1UYaDi|a=yD`}iEA?@ugl=q_awp?Zvfc#_V=@JK8=oI zkvd5aqiTgRz527E{m`Fb@Oq<}SL-J61yXyKWh)QrBj?t#^*|EhSo_7sG_Nvv;};+= zmO~MgB3>OG_t6|7g?)R)Vj!iUNG)`)t>|Lu_rrq8T{J@sBHHFs<^{X^Q0xIr)|0Um z;0y(9a?!`Hyo3whut@*e&{Nz>PazGZ43}UrG6s{2Y9!({b8`bMr|`;514Jrm@ky5S znGgHHA<&J8p~3@^@owgu9O)CV@>3r?_&VyM@F7AQmqzo5+KeU&BSbB(SU7W52C+rX zACR>J7oxz^20#)aWE1lk)DG<v!+W~zj%A?q-=N<)JcJSqg=sV9dTg*Y)yGRsqg@dL zdbUQ&k-cI()#^teO&<Ym`l;yshnmTI`BY_P<-6r6;z*MWFcj0{v+mFXLMJzRK}u>G zz-<p59eeEhT?Y?VL4uUDwB(?G-hP;k&9{4Pb)L9auurjY1XjB@ZcOrqnPW}kfU+Rw zI;h<F1qHjAm=tYo(=#8qd`?JAoCXn1*Z;JMc$dXi&Jg!MkYq(!nM6>z?>&t6p<=-J zrFZ5aiBMD)c>8x9IB=Y{+3jCq$bx6ECDWJ^2QvDI_6JvM&<3p!_Vp>4!aZvqNqYUd z4;3CryP?S488X<>f}stlc%MNl$ToN`%~?6d_I^#|^c5523a%tJL&AJyt*yDB4(T<% zzKW-guQ&T4HN~8ul04j1B!uu6L^WGptn>a^=e>HG2ySphg`Jif%YuGRb{+#25<evi zW!w|<^QtP;yb$t8g)2v@p$9!VTc8HF8H?mYh>x0HHiGM<rq-cb)t?Ft3xim;?1OP+ z&+u?1<n9`1uS_iix67#Fm6E=i=g3F$`hzL1!Zd0J(>FrZ1v83p9^(huIvtydFgY3M z{R3JP`9W13*NwWmy2fYqeF=2MRwGCVX~6V_%y%Q%Vq9*iXPMMvbrwg5|3MJ{sETAz z1e~yFdee)gBW)EO{xMK{RU?nkAvizL=w0VKofm%uE0PSgDVn9ZcI_I8age=Zrn|eZ zn3%A7FMMK>zIf3Xrjj=rIkE~%D~#ByyXZ8ei=cs;0@`6J3k{5=QeyMZq2f=^6`Oe@ zY_EUltC;p)oA+L>E?K5zQ;6yzQwLH4KwB_ds~&7Vm(*xZB+X$(?<!I%K7RbDR^las zJ7#8Qms8jFpbC4@hZP7?&c3bx>Fy+?NA)E;tG4X>Fn(Xw&@dNa{`b0qm3780syv)I zo9gvt0tH(bwehY7<9-0++nqS-bgcph6de$esnw-%V$8+>nJC98^RBzBoB{bP<jUY1 zI+|q0MwCp<pkQRXiz#mwEUS=vFc*Oa_pb(*;BjBkt1=vnUMRw6Et=9zY7PP^ERxG1 z%YJCAMn95_XXfP%bMA8WMdfms`XNJR4;YHzfDQd4!V#}9bM70h`<CQ@u^*CX5*khh zv_R-kp;sl?dM_F1Gplxc@B?J7;;798hOT7t+~u`oCHZhabE1o*LD&@~UC-I8<a`i( zk}lXl8F~2^Nl7y5+S$)Atj3+$f-(RT9hE8b+{4INJCasX3r2S8puc5nrVmU{#BhVF z^?Z$y3cByB3R#^53BrL)XEAG+rZ%K~4&7ybeh>*!4kJBsdHTa<CQQY`&s+;S5;kv( zK^bzO0&OW=(D4KrFN%taR++~|6NB3~Ic_ZFIY13kH(*{TgC*=ad$OOeyrxDmL)TKa z@$<e9(u#_Md?kxJP!EVeNh&BNp0Y~yK49Vyy%tDhU~K_E2T6*eF#t*%J5H{_Pmy8Q zF9~I>0tM7{wo!E;<lE+FQh|(ri45Q}(0hi{s0IiM-0fMJuZP~#x2>1)EBi#M{_^U4 z$smSTM6c9?;nTi9ma8gAi@-%x6sWNuh;e`bykSq93dkp|P;D?5X2xVCcICGz+{MR} z?=Ky}W_$lh^o~RAAZ9{x!bwYuqurdX?~(yl@JNBnJt)o7UGC$LpFXX^BRKr5i{-v} zUporZDG0<MVy7WB$;!&QtgXzG6JS3IE#g7+F*6WdT`<IK)&(?X>&~5FD#e>E2~V5& zNJ$DfHYO9sb}+}##?tejA9qvx7GDhDkQUxQJUqM%cO4%smsXFd*wO*SZ>Zt$KMPz1 zf0xrRPy}3zk6cf{>34Xh`4dOXb;yKXA+bMpZ84T0J$T%!%=^|6FEeqggJwcBj)JLA z;K(-_X@mNkWUx>A1Z=}qZ>M3kX~8)Z$aNFwOH-I}VV+$II3?iuCmUC|f@5EsnLLQV zPKJPZ3{cn;$@e##L1rTULl6tmKSL~Vy8n12&DL|tU|pVf`j3T`K*EI3Jp+N4Cm{8l zhD3%@V`$@@bXHmb)1Mg?jD(E_ivGEWZf)GiMF!Vo=86Fc&wrxq(Rp>(E#%gMW0&s$ zei8KtbQN|FgzIfI+4O9+&yE~k^l-A0Mhq`tI;L$gE`3{vKX7Rn%Wx6+NYEhqZYz)j z`d`&+?T@P~ORX3oojP@j6vkpPs>kpn5XS&<astIkN=n|P-!t5UgKYOU*Xf-k?eYKI z;ADyf!{v)v>5gF8gu|Gg(dxL|1hOMYUUX&V3XFNr-?Sy|+q*X%RR{)uvBgToA9I6L z&mk?G1a_X9o^E~TdKw6$>O|an@)VDvTW`;|n#K}aFAt0y&@G5A{9<#VZ8#>LXsHGu z4iI+kAfBhoXmu~CswTc^I}Xo*AC=hhBRW3oiSJhwi3ddB_AllaN=d}U55`Jd&Di>a zTb7%Der1%^z*4~pVTj*Q?;w*ao&*q)IdLHp92lse4kl9^N<>!??fp?1TxA{&2)GAB zfuoSdNsofv+IZjOBb<ZnM}3n@ZUpj;#YrR9rI?+sFS;LJe8dz5J9ic)PB@qI5%~JR zS@XT-IU4F0xA?+Vh5l0xVFd;$12`36K0cQ0Xi`{E@PA%_6DSUr9r?S_;-I;*(SG<v zfogdkR2=1D04hAxF5`Xm>xk8ksvORxmI4Phz$Jopp<ocb(fBNL+lBc0beBm3GUmon z(pJDPXjFKba=73n&>4xP3RRqH#%&)|%~7WZHtt*_`n14PXj#C8`jE#V{(NR?^L@yo zeV9O8rZ+5<d&N2sIYc0Wq16~J<O>oKmG0ba&<!GI)7wzdKq&PRg^SWKm5UZ~?t?oT zO8{M3S*0M2EC?_eoCeA?IqL3Q$t<M&f`Rv7sj(E-BbqxS_^&%2t=!KPg^YmNikvd3 z6Pc&SEqzN9&rZh2v)~AZh|!vBMF&0ZAswHBddm6s5^pg)h%oQ~T(u&P#vkFp8GKNv zA9fJji&y3}!FC~z>R~m%L<^}3$obK$hYifEZJumn#LE*Zz?SlHS_VCTO-fw+me%Jt zf79#HhW=F^jOKCl7WsS9ea;fxYoO|BTqgqN6bJ*32ee{hi-GSUR2ocbj2m|*A&*Df z^Dv&M#T2ytyM7jK1?1M@&&)MUNI$|900%$uUXZ~(&K41;6-o&ruhi@ehRZnc`wUhZ z3?*_cTg9R9LNy`q9DHflu3Z$Q79kvrfr-KTPs2bBk^eko04VFwYM<AeLD(EXPb7>m ze3i-FnUT@ab$o42K+}XL;<vl>=i*0?Hhg5H*?r;i?$(ZL+Q$y(th^q)UT<h&;qIp3 z?q)e6pb?OgD`8B1VOPji70$i8#HDXM+8DG;eD0C_oVu6yMD^aykLnIRiukdT&y{LB zP&{ZNZXz1n+>u=4q^@aYWkom_#solxjES)yr!U3`M}S*Q&zG!4W1@!E@I#}eLTmV} z>P%$N^GBM7NCt9Z9H}~1G_M0mGCFyS-FX~gA<>T}Px=@1WKe1xb91=*_mCQ9Vm0Up zl4@$<eIqJ(d=%A}P<o)UHhlkn0m9Zn|GT8nBeo`53<K?ceLqeL3y9z7=xBC!HlgjP zWW<W@I-1km0uv70LHeclb`!v@lr;4Fw-HPlgxLbCLUAT=b3bHiOz_Bfncz~4bTR>^ zpihU3xbxzIh7|#x2`NoYDnN%zqmsHHZv+LGEDvy#)0EJUJ`10w>QhSJ#Yb#ic(7g= z_?JgYR+Dkwkxv@}3_8Lw6i3UJ^qVlCq2mo|)$|jp_P*?Vs#b(1{3Er%Xc4#|qA)ur zhZu5@nB)`ykj@eG9RwnoO_3BbHD!Y)Yh-Fljnf4fKVq(wh;cF`Fig~}^9-|E$>C+t zxxF+rN)Q~&4D>O40Ov!q0X!pTN-zry4h<~}^j<c^))2E&qC)f^SxtaRtgV-)Rg9>o zAPAl*zU51d2p#0%GUm%2!Y3MY0_QL7<=|?J)&1;w4Llmxu&-cnK55ri>NQz;IiC-c z3dofJb|w%R$O2*RPQ80~uU4ZWMrum1mf=VO2t)Hb3Y{vwlV4j~E78gh@EC0YN<%?K zdx7|cs`%SU2*(D9iyjr=vWiMMu+`#RUP%o)8_of2=9=4rv$ep)nE#^lKY&gLpFJUa z02}XtXK>m8r&%s1DPMPXhE#_k02MqehdDWGCdY;H9J@W|GaN@$1ciiLe<dYGc?GR9 zBY{D-h(L#9U-k9%kn{)5Kkhibjb;z4bt-BPfyX#(oIe=j4j$q-eCt^0>^HMB9o}mz zumNSDsY2%wcKhOm<1WPJKG3!81`HmI9uwyXf+F%Nq6i1#$t(r!e|4pMjRD8bkU#k= zbM5yZJm^E|B<3{iH?--b)YNk9C{G0Dw}gU0RzHP)`$mO5IyxGgNKP5Ss@c`mg_9!b zX!nqt34<X=9|`Z?bo(Y02AvE}xB%+Se$NLVOoW4wl>F;My9~=8KGy4H-n)^~&cPEK zg(o;=)piC?V8p^<Cnq5(X%Ad>1*aZy-VK3s<m}nTf~W0St(H*Y_7!$2sHlWh^J&A) za&I>a8fbiOZWuXN>FFt>sK|EoD9)R6QGhQ4FiIvjLQcC?Z+787g9LO$I4j7I7YDM4 z=LR7Ir~?)IAUvWNlz&D~#m*6qfQbzxl>Hc>!e|hmoJ^V3`a4|AE+H4<3{>XPd8R+) zNK&Pd0i^lA4P(<LLDk*X(wEbR#<{zSm2prNWE02w^q@sDcJAO54u-cawQ!8E_`hk7 z1Dn9OfssDlc-;lI9?%}|&=~nO+b}YVj);u!ZJE8C){GKD&o0E}ILu}Sj|ir|6mYWi z1PG1kVH6Ei*Ml;oCRu=eh;bhiz&HV)L_@<5<gW)~hR6$Eq8kz7nvhHJM5WN>HRLdu zjY&HP0>r8Os*clC&xWe(L~JfS{{pogM~C}C6v^;v>g8@v@#IDx!}9HrFbM^~m>fkw zNY%jtoiaNwFE}D%IDb)xJ4a*GKCmJIaRK`>P7o}_KpKz78)A5c?usM)N_e+uC#Bpp z0ud$s1e0|flpNY()7NC5aQobtf#<8@;+S{ugrR!x7jk+-KgjV1b_^N_3(wH@kl6-V ztx?JOIoPnT_4OZMaP6FHc!%W$mj==ef;$m%5|UVT$G(5c0~q|^Tp^menT?Gz7Y*4L z+oMTa2L8&C)6)e|YYy;QhHGhEh(#(91q|vF`A3|->(wet0qn&1bt@Ise7-)tog-$T zV0B<N@}{PV4jH!RAKdcEpG2_$Ai}&^Agp|AeKQu7oV7y5fCKPbL5me8qZmgVmD{tF zggOC3M7kPBtwbNqm8o@Vmw{IKY~I1+WeaFQ4qzdkf2^tG`*ig;!bOGc@vXSyhoPmQ zpdcjLA-<<{%pF9J0IAQ-<qf2Zqq|44sGk8HFD#4(=hzbR#PdT@ONb;7d=C>)ZJBiv z4oW9C${HBN&8W?s$6Tb`><kLxk;G^>3W*QmKh-+ah!BYG@sLsEDH>7Nx2^|t(Q~77 zery~MR?o-Rqs(3)V`ZG8g~h7&YKH>mjieOZy<4^Xzzgpn*@1LKzcl_QaWm?4seg|9 zyfumf0j(H8)Ryu2ml-f~bC2IW>g88PPBNjD)JlB(H1P8$k?@?xyDsYMKX=GzN9lQ4 zSV)JFO{-pNEEJ(QK){$7#=*nACVMwiE-Jej&dki%;`r2I78cajl7uCd?A+X`tk{)@ zAV^=4NpLf(B%@>)XlrXP&!%~k0T%45@H%;a689U@#9RU168Q9>sQr;l3)aMo9G>D5 zm~tFdo4Eq-J}fPH-kfu$PK6Aizcw+5H?@Z^{KTsTYSA(n*p7BtIu%CO)+&&LZtx)> zhlo6Ugq79c<bGhQx7DU6)HrCQ+;I+i00bL69%C=GOEx$^@brp`b$nf~q-UbHQnM21 zK+fd;=TEdv?V~}F#TsKmCePZyxnrcc+PcI=_S*P5qp>gAG07M$N!{sKtqQH(h^_13 zhXFp|_B}|~QGVR$!0<=Bh>QE@2Ih1auz>~l0yd-n#VIABWE^5;-GtZYSXi*5;i5`@ z{rdGrr}ba;fs>PyIDBJcab|v{Dx;b=(jW8#ucGjWoCD5+fiP|i4`hsN1DrzOV+WUU z0P!V^*=SMvXAa)Kckj^g<6Dy(ACwsoS2RKr?nu|+Ko((~RNDrfk=T_G&v`p&7#J7; z>rk^h4=a0gZq5h@*ok8d&^Rg#G9VJ2H#2+XkfT{19Fq*EKl~|sID~J+=`mB!Sd-I= z{|)U~L`Ay_7I<*0ctHU1S+7FTFw9WPA4x5AL2u=UYaRx&1l%CJEjWZlXab090B1xV zjH|$P-K%qX2Sh#ik=)>~56b*#9R{PFy#~6`=uOKWpakrN_a>a*?r@#(^mg1cIgA0= z{{;9FHe!BsGh7|QW;A9UKs2t<DHukvFyz=ZJk%Q~sb1Eb(NA;0x+|rs`b0A|A04%2 z)TttZ(es0Ojy=K{k8NgU^?x2o78V?Y!Hc*69EMvOv*&MQ!C4aNl{Gdr^bAo7KOZEa zjz0xn3^<>RO4Q*;)@5C$Z<&8Oj&cVuiPeI0CJoJe9vicUEA}(Elq(S5H46(v`1!9p zIc37MN)8y46B!Vv+~<Dx61ADTWSM8YWK9%3h#Z3k@<O?HMV*Emst>>waM6T=h7#}i z03szmA~G$7o3<9>7jg5D^?^$p7xkKs?4FqTM3sdKfX;(3BRG=4Rs`63+V!u>49+Ds zJdTWH&zfGk33WLW@1Y=O_y69MQG4}IRGGmC>%94Esw-sRgE1_^u1xP_Pk;Y;W#zrj z9d^DfQC8^Ch#9aZXjMB=VjkSxrWnP4v(uW5mv=7Hfu}h2jjEg+CHNHamTYGdjl$$4 za!$*J70zU5=R9o9vxO?|k}o!?uU>5dszAiWZ8hoa?EKwX_ykS}d+<eylK?tBecOit z+pP;nd9eb3a(xYoVgsOAII>{K8%YLeL%)6*042OHvjvhI9vdq+$hZU#H71)FWSiok z0~%(QO}8jHAwFKbbi<GOC8JBvI=nsPGe#9xh!R3NH!vuEA-ez$Gc;1j31H+9PzX88 z3Qi^;ezLcZibYrng9@P5-`yoBkoI{yX%70oRJ<htlM*yc#PUse`^cRGq(Z_qg}4E6 znw;=K$Rkya!SX;%ysaj{1J3})qKRh#Lt66E5yk-Gk$}U_uZ>l=(UOCV6&1iAIDS#w zTQ{o$=f#86$hh&@m1N@^M3MwC#r*u(!A&>)wCgc3CXg5hy!RYD_yC!U#{sY>{f$fl z@mTPElXEjTyM^435c3u#-q*OekEeQNQdDq?=nKYv_x=5iK^8dF(r<t(@4{O$lGBQ- z5DQyi6%`Tb+$~C=GGZ47^%hHBBoI)bL-su699ziEkYuG`s>Qt!7>6n*mp);Ozy<DY zYilzzGqdG#qI|N+uh<r^O+jlVra^!VTj4;QK$tA-UI_rR94lG#qQKFfMMN@W5q;Q! zR6FU%@w9l~LQRH!wIweU_2ck_{K>&W(V(EBae{KU;Nf?8r{>ApC??x_Ke+89>o|Ip zweJ@>M~}8gB|Jd^gkM{xilg)D7&qc=G>n+Zvtrw@(!qGa0Iq>1_%qtM3&wnMHXV~i zVpE6K(t{{DhjoI!f`)+*L9D>lPllp+BLie(p6f%G@JO$clAeS2Lrc@kYey|hL=VMW zKCaQ6DD`rRih+iokDwXBvkFT9KKgELbqP+RNvM{11t3n?u(GjLEK2QRG(+-03J{p% z7_PRPV;?T+ow*uFJYkUhFkk;{7oXcciUxyYCJ(XSI9~;HO{5aM#Nq?2$k%La26Nl3 z3;XPb@Gc?<*GK`_g%i=Fz}Pf3>9Dw}oLL$vs@w4tqlW?C5$|-XS~mNV8{61QbSD*M z9Z2O5<|`=xB*(+Set>uE3DW95hH8k3D^U%R1_aK7exM$oyS-`BPj~s_BJ=w)MW!O) z<pmtK*rK&8f`-iiBm@<V9Q|2XAe{zQE8;1Dz{-sR2K))bO}MZt4+a=)5QZp7k8{dS zX*37B7=#7$WnyAQh9=8<7)}^w@0jAPOPGg&Nd1GR)XAlW0ssW8lANQMdIh>6d7l%e z05BAbh={<?l!0>`ctFg)z+mCRfkwcxA0!l0XMOMp98S}oH1R}WqP{9(d4TM%J312g zFQ^JMJ?M}V4~((+s@rLB;tAmo@(Hgm-%LT*iK2B%mHrnyTvFfV0T)izfC}D(a}GX4 z*ck6r+dS-Ier)r$124zfjVtFB&!(h#bQ|$ZVpK6n=0X@IpkgC&Na|tY>peQ%I5HcM zdZk{>$JbZr7S`9_!7i3^+cq4K##^A4jE?u={)x34@&jr4Kv75<d;>XlK<XFdE1@uB z%@o3SAxpjcb%MM-5oRr7PQj=a@8ZJCe{LXj0D4q88-}mfUR+1axr&NZnEzqe+5L6U zEg5T|ed^48O{r`7w#U_uSt)y5#c3Vl5@Kf`3A64^Q6A_26X-R=GI*GoK8*@{EGOqj zZuLKl5nc{K=S35+2M|Pf=IeP<;jv*La^RxOc;^Y)Dp&`yhD-@@O(D@^T5Z%rZ=5sd zLOrxw8__=K5v~9+AX0yq8h8m>L0(I;(INYR)wE-3GrS9qv07z~fb$PgKX3|Yc>jrA zAA`Y6h#dy15}YrdqL0Uw@LC*nS45fC+l5YP!lsOj_`T^U$m=O!;=$l8%FjANZa&y< ze$eeKE(B+6e%ZEtle1a@UVFH-Fsr&*V*;0+?zh|tX%rJ2Cm}D=$kSrLEB$WNn2ZEJ z-(_ckqE1ai!}=jE@c7no`zM8!82RA|$zcv_zTt~o5-@4RbPLXzQqDnja2v3lCCeC} z`~tjOVffcCu#P{kr={;KU83jsJ(oOwoI5oYOdIVV$|XE!4qP-JS<qL&?2y@Z)r@`s zod_T7;~tJb&#?yRmWTvT(fi6;k#Ty@UyPjBQpY+F$_aA2ckd4^jyqD|<6sd68ER(N zuGtk@h(lC^tPs%OAhAZ?1BQIZiz>?XPV&=I^2HxhyK%>F<SJg?1}zgLmy1b}>kf&l zQg{~OA;g<$vFWlhG9Tb810;iH66NRPJJRXPa^i&SPc;{cvJc`~u}}*D$XSDB?gj}< zgUOI%LlAFlx!&pn5SC=%eS27gXupnc@cV7qx)rX|*3nxMIO)V1Ox}{Wm1gBD(gQQS z&`b3dAzh!2sC5|O)oEg4C0^~C^$iW@rKEh@=gbH%gI~Q+c&y7^Qr6J0|Kd_B-mOy# zAw+~#4ci8V1^wt*N=i*ZejSM9Wo_-zQ){u}1pu^IWn(W1CVS)dtrC;t-zbxVB+DeQ zcF-Gl(a`)<8Cgwx0c4A_0soWnK!ygPE*OBcKk<}JZy!a4$;8`tNas;irCo#Ob5U72 zM6*Z~)aUQS7A>X{I+wQKTmdK|&Vhm%@J7}QFsuRIgSsLpv?epaqQJuu#7%6Gg4$FN z6reBwjqv5L4Z1<Wggy0;1IM2TIQ{`RoI4$#^!4v5^*7C97i;^Jz)0!*_L|GB)Ko4k zDcE#HcXv04F-(b*)3Iw&dEH)-*bYob&=m$7RdtO4yMa>wp>@|KEd;&qbID432?5mP z)k&aJunpQ4HdnxFg;&%4aGZSw4T@jkP7x!HsSx>-%qR=`*nr8*QFPJ@SBi0N+AlAU z54haAVErI`W_SS$9!WNTND2;qIh0wD;^MyHl#eUNer4=xPK4S6o_6WdqxNEEEQR;# zbSyDT<Jc?}j-s)gIfKReLQJF>qaqay$xISH6V!JcL9E2FAgFXl;lYYl7*w)(59G2? zt-JaWnGK)D%)aJ?5*SU%QO}|HK*8Euwi3?HeB{^~&=L%UZ#~whB`2RxQPG`$X@W`* zb+{`-zP7r0gna;yfRc}HlbVdj8Xfh=W1&J=3+&~A9UV1{yl)0|6FbMRx|?wb%l9w7 zC}&==Lqj$dEXq0#+!inhY;dUI%DR$TpcX(DzHZ3d5hwNkLB~S}LunvlHc{)yySZSb zN`y9pgo1zuCE}Q!9c`e3Y9Ca15nZpvqiAi2O2WwjGQY%0_=V#@1ez6jU)b}UoP9UI z`H*nns_*_^XJ_J<W7_`z#+rm|2`OcZ5@o5eOIb?DR&<vnjIE?>*;O=}vQJEw!GxI< zF;r4f5t1k}R8k~a+9b5-_df2P??3SK^1L2K_jO;(d7bBRe73XZeYm7R`i%cNp8!tA z7_(?<%5(Y#ieHLvL55(9JL>2J&yQHA?gSJn#*dF5y|tYpX8tXw3O;-;A|eRmyN=I0 zJ-RNt@TWknS=rguhc%c1Z%$}8+LG32ks>BO{y3M*)o_3m_vj%Y;b|7T<DhBLX3{C< zuG#sQ_1cP|uUPh9<m4O=^l$n8<3};RBbW>sb0Z)j%-kX7#bKi#U5(J6=S&;%0?e9w zl)Elx6*%a&qPIPm$6{Lf@G$QiUcAKWo={h5v$gKEu9R{@+p%B|YdUe=y5VSi18?ul zb3cdYlUd=+-0&WodPD5rshF|>$cSYJeWllP?Y%3A6m!KfBHANdq-l?PRr>1IZ7<vU zHBKs8lrMhN&!XQ$Y1Dx7B@ID~ipV+W@_O~_*D~IDI?a@*R<Moq8aC`OE%VmuD<><U zHa*c{qp6*|X;a*5zq>etb-{rt%9!S;EI$Ly`;Nu5gx1)^r|K72rk!)ciIMueXY}yW z`^4`awl1o4`_FMPvy8C)Ou7;Ykp3)ewFl3aN)V;+-m08d<@Nhd8(z!X>iNvpevHYU zK(7ug2ZRr<YAEyZ*vb?iGCGwt0t$EIs~s)LvSbyM044gWd!UclZbf(Twl=?E3@um9 z3l)ilquoP#E$(oE0|#qenjg_v9i=vHH)=an9d$0ZHtXrrBCV`;YHBqvsy7WYGy5zN zT=*wTV_vrvF$%YhQV$%lyynA2Vd>%ch5tkkx+>>d6p)N`XQ`zJXgnF*J+?#qYxBn2 z42n;Ab<4MeUYsu?WJ+$Pdt&{%{{`T{?@HdFs27=*+tavUS^0*$Y!ZDd8iG3*A(0(q zK7L$K^)c_{JvV^W>#HsyC+Q$Oz65Hb0$~aON7iwxulWLsgwHQ;o%zN;SBH5Oys&@Y zaMCbnPax|WmmZRLDGz*H8U31&@WarnJxe?f{l_@CAAFm)@L|sl2L|tp_lw0@BB_wM z-N9Ikhc@PYKef2LhZnciuSm5Htgc?itiWQ-(!8JOEXW=~1Bhn~t_10lcktc2SN>g` zW0qXvt3@Gn3@Iet8rP+R0z<rP)FR&A-sSXR<)~oSAj|Qhl9jk1xTmAk)KX*vUY=~I zE9M21m{_g^)~FXUW8UY~(!ZJSM7%<bYn02aCbaBK<5J`=K&3)sw3~i7So>$jBmob< zia3+{>N0c+i<}=si_mUT;N+Ysdf+mciUtVmmTQ#a^ZGU6ZD9R@vBXctTi4rR!zHkZ zeR!>A&ODHMV{D3*@q*5|-}{!hUzi)7)ABM>^C+B7TWKXkYRV*nYkUcOHY2X0?sJCt zT({MwZksV{R<fXX*47qh9*dqCW81L&s`#{7tA=-HjJYBEfl0z7g*BTNPwp2Q4`e^& z$L)r1p$w&wrN}d=F%x+tQK!^G1%M2)gaqEMJbZ8vSr^kJu?|FmVoU7h*RPL|b{fWw z1v5B3O6i=XsAX**<hc0HdPQ23|I-41Vz+67KCva0H=p_Y{CO7u9W<Q88BU_Hlqnq8 z$01@ak!m+ltVl$vu#h0U;w1+RaYo72g|mdsOHFZG-&*=zq>SaTgv6n|KcX-ig(!8C zm>dwGt*b4nDGcxA)LAhvalnsz9hcNjva+fW=?UuC%F?(p*Q@j47<<Ee2~t7N${PuI z>xMStf+Ch{-bsN=jymRT>XXEETVk!l3Mme(&OFx8(h8tk;9M4TZFmTyh5QIV6ieUW zZb89%zpaSM`6TvkbYYdqGWH!i_VK?5f75QloB(0RLZKZ2cAzx_mX-GwkvGlY%KU`m zFzZ9GZT%=(@WUUV_cIgOYI=Oa4?E{Km52*b2a3vzlq#wqVza)}S5C&3$tV-&C`J<J zi^WJD`(Qt0{8YU1ZRZ-Dv#}&Bgkii!-R$~EVmC3cUb)gS@OGi%qbASv9(aZD!ES2< zc3++!!4mZou^DuiiPbgw?m;g<ocz4D+`3cqK<5*y3%vUA-En!P+1-0C8`_PxN{|lW z9hY2TfN6I$+vM>B{6=mH3twu2x{NI1!Fu$lzj{(JXFzA#jfeR^_UzlY??b{*6k$>5 zr$pr==oXcHkBokRy{yJG%_4@zw-J1D*^7Ud0kI>1j|EwMS5hJhFuaYR&N51<T7Yr^ z`v*BbAHtf!o1)!sdCm9l?R9jf(P{&?b2s67CNp{yzo&bcuP@ccq6mX+{`;dJVm;?? zI-g&@Z=)GML9dkgAW&39;D>am9z>fN95f$274jaIVky{Frend5$gO4?`A6nQ+*xv^ zzx9SKEE(b#E%w;vA07R5*oUgayy>a?Y|APiIR|PEOmdAvwr3nSu6c_Vq<E+*My*J4 zkKWd&=a+})n9bC6Mt!@y_!Ns0%Ak~rk?RsRI$b}!38n?`GvrGWUHQ*HN?Xy_<5e~3 zT^K1*5I8hNtYK{YFv9G7$B^2rlRuhC>EZpnc5ckrkec@wpY$+_jJR^;^JNns&)3X% z2Y>(lt%KUeznSS6JBimOH8-x#RYU|&JQG{^&rxS~8n`^elc7`k&#Dc3UKU?`_Tgep zLs459q}(}#$)_)0&Rk_56_9qEsh5)F?6l(pc05Wqh~FC9$;fX7NC4agsAwzAE%-M# zrYO2hUwCzXkL1Or+D08ZbP)KP6ax_e1P49y+jPajNbqcGCy{<k+Iza($asP0;l7#` z852SUmKLZ?GwF4e@u}?PSNHfVtI0&&LeAe}uuf{-*iXkaDyHvlr~6h`Yn%Xykr8#b z)3Zjpo_e#saLpx+`$wmqO}kU)U+xxvE2*Q;4mk%1ewJCZR*wcwckA5nByN2$6dn>d z+M$uqr{nI`Ppx=5e%Z|S4Sx~52>%lmWp~}xD)CjT&;+ODJborFsk4B(jVBy%zrXyd z=q5xx$ZXW3<2nCLb~Oq1+D3jK5a{EurIG=vptLJDk6#)a^SN%14Sa&hifkh)PO%99 zqnPf`YSZKAHGU}j$%MGOg_Y}hru*Zsig`EmZWZ>etgfDTF3XO<U_lk4G<2vx)1z{q z%tE6Oia1-+b;s0|Phjpn9v(1eHY>u#tzQi1J37w0<g%k`ZdFq$yqTqVt7vmVT=Z)E zA@GKF3!2nC5NZM(crx-ZVa~~BS(COXzvXGxrg9{c1eM=o-Rsd$t@YR56ZG80FvTa} zVkrAf4V^aJ;CTb3Ui5i&EUufGyEdn;i+V)*Dr%(rwNZQN87dH$T|)1~1NbXCDZPJ> z7C&{_X;t>dHtbg`1#O~8nE_Kmj2#LdQwzdJ^wHN(>a~6X5RNfDJQRIJbYguFaO~a9 zfn&UJOZ})T9!rF|K$y&>59j!W+I2IAp!3|)=u;I|p0cypRd3Wp<SG23?)Q0f#|ofR z<kh0Vvke_COYFmwW6QFaey_3zidGKDb?YicghwvV-fqs_?;i4NP1`<YQQ;as3&^<! zu;Y1en+^ZsJi`@8D^vd8yjs?eH`qw@SyY(&FN0_EIErf4wWblNpw1JxR&;FK&C%6g zhHCWhdKF|N<qK;gx$Lu$-rcg;@GlS}qA(Vxs&|VnECr5XEKG86&=MRS2@e{XDKRn4 zQ8Onu{3vU<45km7=kV_@`zKXz{q<r$21{T-!0HR?WItV4q^qJgYRaZfBN0_PL}YJ` z3jHX;Q$cQXKEa1<TgVZaI$L!yvjb5T6j(MMU~*-V3Z`I-&hO@2cQEs7yv;3(WoHUc zQYj!{qe8QrHLF)V@Czy4LU>YiC4TcOdHp(heZuD+%q>Xc-UOMwbU_VeYP9B{RBC-} zelJHD$!vO8B5lxRrmp<AuOhT<0ScnXS%1$CcJbuC9=+Lr)J$+{Fk$BBs?Z_YV08kY zPG&X)jFiL$@orJ~AAPjZ7b<oJ^>e64!ac+q6^NxN5nX79P3L7b?cb)0am&^vS64e> z=)mbpeELqBa)v~%GinFkJB9>9l7%oz7E^>KQ$ab0hX#$UFPSbfRDOEio|-mSqodJ^ zkKbCGlP{qWEaO5<NklqR_QR)EV`Z|5w3$4)Ck_if12yN?Wm<uqSSQKd4h!?A%wkv` z58v=FtHjoFU){mPWw3s2fbMg+?%kgm;qtH9YZ*<hNII3$VbuCP$w|>ggPyzgnUl`w ziuFCO<b2vQec0T&Hk*6#&D<c!2^(51-ktT4`hGbh00~$Q8ynb{gHmswffHm`2dVHG zyHb~6z1qv`^f)i?lfr3p5P2B{KO~F$1lk%fT=d^qd|Ub6dZloxny}}hhIQ)Pl*3`b zh$M*vdNPAUsnFdgeS%1YAcvZfc;foV=)#%2iXx7ddna;Wbe$-%G7z;0%Zc%9&)BFo zmdwAr$d?E~7QHcd6U~BKL6SeEbamRe5&Qo8bbq?poT|{$y!(!C?G=eMn^5C4q^*UB zTYmMvsbN7iLN)EsI3SlV;om#kLwrb8MHMN8dlA)3-Fy2)R}2$kIHgvI9Y3PvxY;$} zD4}N<V4SIFE>d;39m4?<yS}QI_0|yM#0iIn4lwT0p+gc%VKLN#FG;Ezkp9p19g`yD z&H=9hr&GwV5SZJSgdp{>Au9&zOVp;4tR#AoSg&G9wi-9?=Jv99f=^jH570R`6K}w) zva;z&MWBX?DMqn*rIy&a_FVn5L%;{9E>f2J>gnxhMD+VOs79|N+gaE%9yEm>#lnVh zc|@j7khzZn;eu1#?~wcWI#XHxQ-GJ)!KC{>o4z?OUYKJQ8o9pIjTe!Zu-kU>HRxeC zqWObOm~<}d=g~9OJ3$g<Ms@aF%c?5L8JwWJQz8aQ@0Xj6kO~Eh_@V;>R7oE>9<Hv+ z>^_n*Z!w1z1B|4IK|57Ocx(wo!~$~IGNm@=%9Vp)l{_bDZAE(AShcB?Ce*qMazamc zOs9WMQ+reRmB%CN^gbIUbfrZ=h$VL(I8tibefwT1Yy-wDjn4T_FFp-~5V94QFQ$r8 z#j2Y)Hs3-W2*<MsgOTW47u3JEr(!apY2r*kB~x7nSH-RP>-oyJ6!TE(5={%aEI~xn zsZgr~MYK^I`*Vy&lk9P8lYN`VWS@_@&UH)Pj_}NcGxz2>cryo3W<n|TgC>F(|7n=| znqg@A#8X&X8n>6_+moe9B4?%f1}kKn05bN@E_iTDB7PvyfwGhs2e*rplf)L9)?99& zm`SU{G(zlH6N1`fX{2>G-&$vmgogYCP3ryP<rY<cV*<fJ1)S8|$mln!)RFdGAE7Le zty@^xm&z^3iQX%^0$4LoW)~TRj5XeVu=2gFx}s^7-~@a7R4!1m%_OD{?K?s+VMJ+? zuPjMV0wn!jV4-5%qIFlb?{6tQQJgJ?zn2_aQFg)XncH_fox$2IC^@(*3j_H(mltS{ zS~mprPa5z_+7GFX<Q7U-M`<8b=7S?9yy=p(4vv^>>IP9DGFOBwJaU<%kpf7LkAxG! z4q|Dait2V`*^PsL{@F9Oo%#&xA=_Dya<yPngjWX#vBR7RbCOr;+N(t*w`kE|Ex6&O zaEE-;fa^!p-Qo==<NA;uQAQ(gwIB$vm(s$3jRAYW0?3X;yxL5y(RB^i!oy{=9m1QY zA~uyIXWa7Ov?Ar?hj7V}Xxd&=^A52{51rQM^%)&*VM65#-Z=!Z8WZC;%o39A095tx zsb5Udv=gK5iEbQ_jQktgSM_&OLL+5c4hk*#U!SpKPvY<WjjnwL`w>az0;(mlA^=Iz z5n9k|v5dhR(oB?BRK%X@IBw*~k&H*F2P=v}`>2n1ajv|-mM|SmxZ*)((-R*bpSuqp zNW4Dk(0e-;Hd(#uL>QdN%)b@OXDb}x^U<Zy`tmKX3rPbxR7(_9Ok%Pd(!hbXK3S2J zD4OuA{|kiRl<sC^tk-^Y6>lh&KUEq5kTiN={GC83p;}qrjBB%UI<Jb(=~0lp<O*w! zE!6Mp>A=k_<ub1Vy(3DK_zI>+o;iIuBNb#*09c}v(e{It?BX4zt8-iG;@LIq=W%wI zVx9z9XEr@PKVk{dgp9#TEL^xA26+v%Jb9lqdzegMYGL`PNQLfx>KDioaugvAW!hh7 z&iFE63jD+DBwRI*n*Sik2xkLK!n@NR4v@~8&M??H=w)a;JJ&?a6)kmX=~+Hc`OKha zC4-NmV2ec{F+U>PqzX#a67-Am1$<p<c}!m{Z<v(kUNh`sA2xLM_SUzO4o~zq<sOOP zRpw64Y2WeV13gwgUWyPc2!|y1p90m=$I!hA@F=#kf^Qm9|IeG(l_HgtF9-_@=0b8d z-~|z0AhQm><Z^X>;+laF;-J<%5;hS4bl?wwC3FcYUA~?I1`x{=%YcZYfq+0(ceRaE zeQfjCX{z%Ev)w2}38*e$0ToJ~6>fFTs%VzJ!m}f>*dx{ti;D|o!IY*@p*ZF>%+uGO z7Y9KA7$&+B!xhVyb4NXmYA^6m;fG*l8qq=?NUCmJUhv#-t8-bXwP3GqE6LY!3>cVL znJ4pgHV$%cV?UzHn>pF3RGWx1UQw>ftj@ZE;O=;ME6*&BgdV)PxCgj%sCyiJ3~RPK zUz8Y6`#dv%cA)hH({oA7ueuKHmba#VxYiMcnx^%$OKL@Az4Cs%;h^I6mRwtEkr8rz z&zTpPY#HA2*swOc^$7?CsRwd;KEko8YoD&;Ja4QG>uwoCFJ^)Vig(3<Js;S`V|Ao{ zimhyu!Yn7??74Hh(IOrsP2*jmo}ZSvvD2^K1JP?{-bwkAhIbv+ENJ^@j5i{)Qdk9a zcc<#(vKUg#MB)Xh9;jiV*q_<9g{bT{(@<enK`oHXH;7FmA|gVj6hYGOqV2*OEsqtw ztuM0c?>+VWVtzB6g~U1V<sF}Awt=C&SS_&q>_2ww0ip=ZEq{7-)^k?{Qzk{J1j>PK z35h;};y$7Jt@I)%FY{<_psrb%4I(RH0fN&9-cG_*Bz`roVR{KsFzd+k(wQP;0~z{a zCQxpqg=$Fu`JFREXDg-{71l7r3Y0DV3TG|?y}^TLtQ*(K>;bEf1CXsy=uC!kZYNF{ z$M3fY2p2y4d}?7PB=YB@Q~OGWCMAIb(Q4QUfnD##{-I&aMp&=wzvh$8(){ZDbflw% zN&Tz7LshSnUFA=fo-<eXi3sqR7&epK`XmHtQ!wF53xz%%xM8Gy=cGPZt=>=%Afn<y zrK9F0HIaL&QFtBQdweUaq}vgo*D_^5xh10+Lck(!KbB#GYe>dqmlVojDl(z;sl^Ry znvtq)&8N#VX6SV}<tVNymOxXy`ZTp0Frk01OdBLa*j9F%0XLQWHsO)B)nTMs<F;Xz z>Zt(39<BHE^{75D!}CLl_XznXMg_61uj0SHlsr3LgyMfJ6n2#Bl$&SHpC@BNOL+t@ zuf@*(8ii*O!1kU|H4qe!!kcVp3Pj2FJF&H9nX-OUtLO}4CW1pkND!)Te6xR&1acf( z)8B9P(M;RRofbGLN?NoxJAr~ZzrJ)mWBL_ZJI1{P?!mXbQOr@@{nAOnE&)nw0tFK2 zr}FNth&}KV<Ibkzo&*2tyN_aanmk!GA9jy}xPY$;?Fia<-{eE#9^s<@nG-HWswCP8 zwK;h3BOb3s?u<P<n;VohX>Ueu3}GL8bnD+u;!R{byk94tDfqSDiG7PAZ`(^bQir1G z4B<+|tsNjtTav#vwri~ynP&SbN@Gj7nL;`mCd9cH9DeFqU}3He&LX6h+&L)JKTzEt zp|IGf+gi`PmDR1nnQJ`XZ-SoTzd3LHz8FP;n5+@pioKf>xcI%Riaf{f^<~?p6Pps) zWzbtOt6)!l8Ep~ZIMMUcYG#St%^;;Lp&|-_dlX?D(*hf6SoigAI|HdvAr0~?zyJwD zCY+WGrrxB?R|Lgg4mm7p1--{+@Z4j+-D*dkm6&2#X*37Uw?szmHRti(Rk<<y>8T%O zXWz-#T0KMpC}B3fMrsuPKC6?F&poUAqf5T{2$n1_2zMD!rD#)rt&SkDbF9Yxi(3Ds z&xe7cPP#XJYZCs8#V6JDQresw+SK4h#@Ea{hlU%u55$%rnQ6)G#y+6h{7!Q<FlXCW zFP3jH3)hvdPn4+zpMjl)487HhNBEsKoZ)d~-;9EnoW961Ijt`ro_!X!ed&pfyFl|{ z&W?q*ZZ{W;FOlgD;;s~K9akVrQV8Mw@4So``1e-Cm?2aV^9W0UxT6KxX89XcSDq}I zF#9e#RCp8QL}cR{d64pq@K*g?J3-YmEBDa7&`59JfP^CAuwaE%+qQiR-DHT?Nz`d_ zGQ51n?~{DRp(JDCH6{DzuW|WBkO+Ov%^jxo?`)Af+q#L>AON5ZnlvXen^7lkB#3Yx zHhBo-Q4MvUiK{fX@8Vv{`=%6pr~few0k!ky3M+Aeaf{#7_xFoge8}6>fVpM%falpc z2N})M32|u>f*`I)Q6CUOLR;EH%KhD{X0u7&>h5vl7G<l5v>M79>c>LJ5qtM=j`B_K zVBo`iW7?@OwvoX|VV*L}-+YkftU@xLk$+uj<gt$F>gW39xi5v)tHUWHn}xuAh@bc1 zp+mh74)xb?w|V?@%3S{q!wCnZ`~9}J;afWi<YVF%a!sPNWc<K}9{6>eJASqyav)c% zQeexUU+Xp;VU;+^YjbouPp~6ZI&GcJE{jniNVuWUP&W@jYX75n+mGFHegM{o#7N8Z zIB!^CUr}2~kO+mdRBidSFTawMw*NS1wgoegp-wFG+8fvGvG68>`~xOSLLqJ|kyG(1 zpm@D`C1Z?@kI(hb$teS8VlJa;NCp>*dcIN&G7=(1CZ+Y>0~)6@`hQXYzbWP=56BJW z{AlRJQ?ASq0h9i}{Q^;Bn&6fExz88P%g<K|tRdtyj#Am4MmCC4^6Jxejq%giGL1De zM#IRm0ipt#s$jDiAAuWECF37*brZ#1I<%UtyL;rv|27_ESbio>LCjTvL!#r2p^F6j zJLIgU=QnAlwoz|I)pV#FFrx(Q)I)G7z1-HYMedZmlzHp()0KeR%$dh@`zDCwNhu#f zMu(U{bjd;*(U_TrSf~~}77Axg)Q6LFbHMb}@0Ex`4iFWl%njiklr%}`Z&a}f+PAkh z75SXlz9pUp1VYT$v!;(0*Da)u=|?@k)i#k+ck@sV4edSZ<is=M@o9i+K_A{h4<M+P z?$&R5WWh_lLypjl;6;gTLt5;C7@}HGLWTV7E=)S=n^g63Ph=YXiP+)5T)(AR#DNA) zlrJO<QsTN<=oX=37WqE6R2DL+RJsq_5(IB7nh%WaH&Hdw3dx=b5?Vx7#i?f>`pi(( ztLb62-v8xby;H6(!12N9S8>U(8RlX(U=r}6Y;*BQa6s9xR{UyR`^H*A^~h<n9+6E* z%`f8-^=nT8F`D>moL=D7-@&}WNnM@lo9bLluRxj^%rK&U5pz9Q6)B^~m@y?;09Bn8 zyCHdXtp6i@i6mu<k6+3s_OX7xvM=0oOPvkd8EO#UJ$LY^q7-1mOyd2Q&8S+yGJ?es zUhmXy9apeA!iXiFUDi&*ccrQz!L|_m<8?x9=^}tQZe8)&QO=0Z5k$zl0oEXpHS%hp zP0TrBMJ1C3FRp3-S*isU?)S%NnriDChh>#KD)Hvy318#OqOjj1Vw9|Gu_$v@g}&{^ zh`$hNisbRL=CfPWzDY0Lz?pzKao6mno)AJCL6c^n$m1zD1SJ*Poz5Sr4V1%;1%4V$ zg9}|eOYHT02p{2HmT0ENUcC}xdzPbP8VkxTH2DTbMsLyvCBC`(%vmZhAZgUbVut`m zQ3|=9kFf{IKno`UU~m_w(SRcLqRIybMaW=Eco&(g8Pxld2hZNm#8M^qun73cwsDzO zpBi7gu(4f&`o~()B{W^ck}S}!pasFxe^538lr1zp$~M6L*rI{U=OhpXqH1V{z^X1I zcLth6Ddm{eKq97sr2~=ztaHcbhKpkmlAAL|D;h}qO-Z_w!U{P~(8SmK;|KG=)vnHu zIPW{&^U}NefBv%5Yb5&Gt0TS^QJrIcjV4d7(eRT_sJQq(1dw1T0V$?bz?=uNdmri; z)(uuCB0!O6>auw97p5Yvk2|6n*gQ0S7uFcMU5+JMbZCB-EVxznMFN&MgI);*EkYSi zJN)T;hb7(F_>Fzq1UpbdUgjJSxl?fb7`G!6RP`bsgt_RYe7Wv@x7kg4IW4Wp>(*WC zr2#I?1cXqafU>$%XVocXom5oRW=ydhKfALh-f@yv=vWGAa)Spya?FPbE&8&t<U!HP zvJc)PD7XM%7=`-!`iO$`DE=sUX|@8CuJRE&=N|upd^vbuud<)?QpUZ#f8Jl#86RM? zC$;wy%~syLeN%S9WEnWq@08g%MNLv^{U!v6j0jE)(v9hh%_A?~n0E2W>+J4YKAUuU zk0N37X<oUDUKfRDc-70gO}i})#Z)yPba4)?RK@H8MP36tJH>7qlx^L;bK%?rgV~<O z>|~~0YpYq$FFrY}-0-Gg>Q3bzL<zqe_R<7W^<xgFhYM^R>9RGBp(pc3S=XNO=N;oq z?)ux@YNF`ARJQ2xEA}dSdC}a#z;=Lg!<=o0mLL^9{WUyumvRfBM1y-O#>$<HEYwGt zEjIY;Yq+1f^7uhOVX|9J)4hh_7EWrKnaX{1)D_mI+Y4Rvs3`fjXQ#~_wy$u~QyyB^ z{o5PctMl!K$;Yc^S&!80)MwYrg&M82_~(?HVU%5mI=P*YuQgfSziS6Ox{0aEZwSeA zx9>Dy+`>yS-l~=syW|JT4dNRNDQSZGubM4CQ$NYW!~cNxp9YOO^sA>^{O>qwnjtOJ zT6K{#i0MX8OO5f?LB3GsEcK?EW=|X0>nLEy`&}7P`%Wo0gff5`+UG2L8@aynl)BR8 zah$&8vNOu7*mmfRcn5vt)(7mcEva8%cS)s{><{G^Gn^`(?os(|DL2nli9Fh1mGT<u zGi>L%4;U@mFmb6>Kgu9(lz;Y`u-CS%O{WfQ^T_e0ENek=aA>%4N24E;lz;O{9I#5c zT~z&U?!!=R?&;+7^5GW7U{apS<M_pBTHQJw(o7zW>nl6cW|=mCRo(`cNy^($Zji3R z$WVUc)fguD40U<<DK|Z{c3Jo|G=8@g*W!6;d!X_p{j`q2K3Yw|k7Tn~`N7C3c9-@k z_qS44gsdCfIKFh{S*nj}eCEn4uvw<b3i2vEUb*9xhW%*+hr8O1_wxPmMW=eVjN$r* zDmT#D!4Vs8UCOw|hhg2FHeS!N;je#XP67fj{?xhg<R)P*UL2j%N4fJ;)8AOmkdiP{ zD7?G!G|pvVJa4ogA(!TyPV2()GreRYh*SQEK656mC_1rUR;)n5EnhRX3##$x=;+VU zgI>?PB;pLn+J}w1Qk_%IMTfD@2wM<12nDtZn1XzM)vC3|9h9Gc3VJ+gQp-mBd~so# zL0e4>;FK9%)#UcJ*hd;2^U?Q)7jdB{`d%B{_Eke>re@P=nLB1bvD(qxLjRGLt%=Eh zF`54PxBBGo98@^5jjjqCvQM1cvrqrP4z8U}+c{bp+0~pb>a*^x@q-PUre{1jwyfoi z>;@0B;*AdlK>9R+oY?6nC-&~pKH2tA>()-sm?a$?5ERy1Rem2;U0q!lpV~wPs)t0B zsJOVmGiQ34psKL!@s2o&Z<R%@fk=E`zH~OpwT!(qx$i;giJfn3*m3-LkH(*STVAg5 zeX~x%YON>-^;0t@PhECe;gIs$Q(ax%Wy_Wx{ublaQKZ}L?%zr6Tiy2|9e!=*?bIcC z=6(O>D!&B>pPnC4NY2rXXLPXVlNVo^x6;F-*U_nk^gFH&_h&8KKkb}}sp(EG!PU#_ z;Mg)-OUoyxtiw8(9#vOQz6tX2C_jJD*~uFR7;#{by7IT_-Q7&P9<uIZ;vWzYFydxq zrRi`pGf!d+)!VfBb)n-4wbm*}jvl>r_pYkWn9TuX&~2`*ww<b{uYY>%e@HgUzf~;K z*3ohG@#*I4>+8B_V21zgH*b1pmJFrdf#KSI_;BZ!9^*L;z57L`Z;OjzeRkwTd&sWS zs=Je2|KI~wgSAqK(J>i4Iw&GyU~b&{b~ZLPb-Mc8)@?Ct#aW2#XG8VJuBghk6Ar4U zvo~;A@J=s&xt{56oAu|iZX7y0JG;E*%L+^V_ElfL(4tQ&*%ZkEsv4r=<AePDn?K0N zn2<H}!NZ53$xU{Q8#i_BlQqrvS%;~p>{{3}B&~puySqheByxpl^1<fj=c_^s<@2>4 zKYc2Iig0jnxS5&Rf!nrZ>C*jE`#yeJ^xd~jvj@Wyhg|29h}k5pq%{dBzGs(B4x7nu zt!~{cj5K{CKcIA4wrnQ{dvkGq1Qvw=ey<!J2zK-*kCT9#7_Z{C6DLi&ZG8WQvC;OE z9w#qcxbTWVjc-*Y9o77sRDb<yGHTRs02Hn(SE^TR(0=?&C&teg9}zm+>K=`W&+DMt z%;k@_7e^%z{^NyPZs7p8v?rxUBKD=fHlJ5}CHs5KuoH%3zZMreOsjjbcfsqSBZ5t= zA`~6_nOsct @6&HiVb!H7>;s}f8js-n(U&q=?sA$|R#KhA!r89v4=?a0|*eSMoY z^BvD`tzADQ51w)HnWwk6ANwlnhomiJ)T#{T{Jck?Np*K|aT%wT-hF{uYnv#Cl=trk z@L=|36j#*OuAdz~=TO>WLJXR6-T@QOoSz&1?!$)(9v)@eG;h1___*Qd^|hzxZz;H( zwra7_tECB!3p__p&H6X}a8%mb9FJbfgH^sQaQH7MEYtYm?GZXFBK6};2D*E`%GdV% zloEUXj+>jwywG>Oy^bAWSEw5~Tk{H!pFKRR*^`>*5n;0y#2xvvc|flZXENVjTebgc z;HtuFPy1eTe70;+oKfmB$BDo9TG8>|+t(?^8AEM9mX@-;c_B5=`DY&z_oyWgW{7z^ z?T-8)yI3w+vP2{BcFMx}g)0_5riVQ>!{8N7z<<x5j}M!P^f~#*k1>q>saci=otDq< zbhsp7ORjlPNl~-m2ZBl#YlkO(={d8gPnYr&WhrS6&TAGQt1*kbvdqWun&p5G9=G!< zErY&aPTlo>!#^8MKUK|H+#}R!;rh&Jg-5#Id-%z7#rl)ZHJk2rZr&t)&>Q^<pWm+r za@CH#<|(rK_If)nqx;;rVIC_SAFS`?*z~#9v=6<;ls4U=(zb0|4K;s_w!7P_`9FI4 z)PHjCop9ERmM>R({ra_aL&THtMTSNJ@4kLsvoWq{#O>Ste(rkihyI{g(@)j*VQX^r zpN^Tfv9RLan#HR$&uQ7kDd)re9>>bsG7acgZuf7Fh35ON|Ihz>t9xO2b<d_1E1RqE OYl_u0%Sa2Co&OIC0B|e- literal 0 HcmV?d00001 diff --git a/docs/experiment-variant-example.png b/docs/experiment-variant-example.png new file mode 100644 index 0000000000000000000000000000000000000000..ea63e8b196b7cfc01f6c92ab863f7e271a281d37 GIT binary patch literal 46792 zcmX_o2RxST`~D*{Qj}E@DwUNP*-=rbPzlL;l8lm7RwBx#>;_6y$`(@gcx04PR)ef0 zN=S(KA6M`9_kTa%Z@n+ibKlo}UFSH?<2=qg)Zm~FJL@`D3WdVHf8QP>3Wc6Vq0n)& zFySW~(--XV*Yb1wOx-Aym38F*bn)UV*Hb7$l>K`&k9b;)eY#-6)v(0;Q&FJaB<)So z*JZ3BEWa<;T@5R5IGaXGco@20{`a0(zT@(Ij_<mtq2+rdEZ=pdrO~vqwuyyo6I#}m z(>fkK|NWWDN6`yu8-9AJwW?iEk$Zlj<k%?<xx&Kp5i(3#EW$w?_#^#V!SI^@{d>^0 znxJLW|NG6Ab&RxsAHg52J6I!@|M!I;E(b2YHUImN=(}ywTmJVUqlZe9%Y&q8R?>Xw zJ`=ywY`tz;<SLrd+H7SGEZ)@rl+`H5x1w#H+j;V0)q^Sf^XJbiwG9ppwn&D5kdC)I zYx)ZRj8W&@Uc7aiGAWyAzJFFqtk$`+r^muY>X2gmshlj&RlD+AG<XsP`1xDDOexA( zng>fsoo#*fqkDeS=E}M8$|a7qYeS!37~d!^&equ2xbNUWZ8NiVZEbDWl9MB|vLsFy znT`0YQk|D<7HoF@(pG=wr{dxTJF)0WHRtsUFW-&!y{)rd>^ZV_VoL}8m&u{#jc#+3 zwu>pP!-;!+&CZVR^?fEX(_iuVtz;>0`B*`wyk5f4_}-{PK5q&#QjT@rva$E7n)GN9 zcpMTv=X2Qp{$$;N)Zqrt`%&G4Uj651Z~5NxUF-K@QnuBHeWBPQ?(7e*?DB~ZavpNW zwDlF^rTkKu0;>n9e(R-lc^gZler&R{v)iexT+`jXHYX?NoQq4qty}BExW$<i6&2s* zoLGPK(Gksfv7?{+`h?Gn9>}LoUWkj9rrmiva>w}b-FAn&Mwxf;C;t7->GXz4pSc6~ z-t5Bvay#{gip(T`d8<7!lXO>3c3+b??OpCJ)w6<$%JD)<j>c7ew{#b$=kErg?W5D) zdzF`L<);JXXEL}O-wuwrerdC0%wLxOMQKhjO){^(^Vb%W=Yke9-e06o{qcXnuc9(Q zCwA%ec8y&Z+})4x|C&<Fj|dE;r%<nrfBr1Q*wasSF_jRn3>^*e`u+R&mS!boWwEyJ z-@o?_4yJobByc+W-YhIEv^n1Yr@#E^#l@cL>e<B?w_HX$B==}*Z<LiaVhzFKEhAvN zUYo+WL@z%&`naaElb3A9_xDAH<=!3@UE<TUWdjcmrS^Yof4uYj7~iYgN`cI)HsI;2 ztfY56ZWp_(Q*N<j6u>1MbnTdwB0JlpFD+@~9UkZH+qbt}9BPX18yKK>c6Rpk@u_WS zVE+BvoyXwp!*&O!k54(*E1unR<Oo;l8KcG+5yNNh_GR-wo^P|PP-W2cKlmUpIy(B@ zz7|FKaQk(UTXX^o$|8Kbt)#al)L*r$5H|CRRoqo+pCI2f^yPwKmuZ29CHJ4(7E*c| zZu$A9izXEkmArz@>oe<H6P?rJ6J;)}DQR$yJ+!8o&Ob0PU*=Zj#AtZ)q<el;tW>z- zoa|DPCC_+0&DrZ&(v~0eUTfzH7B0SM4q@+^wEef~E12kyh;hfn#9WJu3)*@pMMy-1 zF*7sM^Up8Q6!nW?adC0Hi>aNNhnYV;^VnT-_LISr(}wCQt>wj4J4Hp8e<(gv+Zemm z<?kPl&0Dul?X_5|qViUQe}YYTX)<JFQtH~Qb@SVyxu~e9FxN@jIbQ9GlzaCIc=s<d zt`VHVI{Tk3Q2o>@Zu`zhMbQj@^~)}q4SwJfq^Ai!Q$18+IaqhGQ7&<%YV$y43_`(@ z>YVLi`!5LxK80UO>Zcx_yWggtJym9_{_>rqqU~U-<Bu(88$Q0Ecbs&MZSAdd{n8<~ zW7*|iCd-sV15^5Hol}!_wx7HAvAYZgKb5Ftj@YC!duIKLFTqPwUJGvF(L?iEDO+Qt z!gCo7{0`O2F3c1P<rNmjULL%E;8vPrXU@6v=f8w=J6ss=iC@+q5wU_oTk@WpVWfQg z`0@2q7pnZ3_gvH8+5?|I_o?Vgj97aq)%UfNgK4f})UsHqThG?6Xgx@`x;ZR*Xk^m% zu;PTGcmikQ(A?zoT<_e}S(~*kf=OmNEwLJ}zL%Hf#5)h{)o&m5U1u2`K9}$@`~wGq z(WLBsJi7iFJyo9m65&IM!_9B&8%p;p7OAPX9`Twm(Nq25ZLzNM%)}NcajC7AiH8)6 zwpXrPDqC<%dan*UIJD@SZl!$T2mh;NmEe#NmOXp+2&t&tl3GSN=js}>Y_O_o7f!Zp zvTAm$yU--xhH>Qv)faaAdoC?qj1xV~l#-IN%_u!Y{zn;p=Q)~P^7Gfo;UkZZEJsXk zd)e5yKYy&hf7#ZpTQeU7`0WegNZY(&Y~6|uMQw`Gp{Z+vdCh^G>IT<mm>i1C(lauI zVLkX~pT7PcS65ls-_O|zYWjl*db!6>+9eiV&pQ1k&B)gD!9jt&7S?Xl-;OT){@^@2 znp>Iv#HK!Mf3*zdNTw0vft&K0Z|@&?gY6ehChq=ryJba0p<QeLuaUPqeP^Xp*{iz3 zRWJT=Kay=0-|B6jD<R!eV9RElV;%}8-PV?zoZL4$8oo+>mU7^h!d@6T2{PMuBk7&f z)6<I;+n2u=NBF8z$kokl_o-9&pO_<hxQ}+Qm;20UU*EX*qzGF@vdV@2Th0{$2OrQ= z;v|prgocJ1`30{OkUsf><K3g9db*pC{+!w~^b&tI2wt*+)A;g?eg7_V=Dob{uQxir z9eRyZRkO1b1AKC)BGR;&{BQ1Xw0ZZ?5L<9%<G;t^Og<PqHQYLYgP9#py>#wN%UaP~ zY{0(#`>!W3YE~W6_!|T()wO5r#z*o`Hm;`ZLWtrN5Re=ya_n4LdzJm3e-ek*z6!4O z>%%n`XX!^fvIBK9j0Qc%djzn9&4p9ZQ}WU&8kf|ycJJo47Zw(t{q^>cVXlSRUS%Y8 zn`}1T?N<0;SAlJK+?Ow3u5R2L_IYBUf#Uz>p3XqCRAt;G+fBp|tB+4zSXQlKreMRl zJ990WDNoLP;KBWOBk~SCCU9cT$lF_)N5aH^s5w!*P5kJiAp5uX?;76I4s&-L+;6q; z_06V;bvsx&IXR8Y#(T;q8n>oab#@*zj=`53RiXtoXn52-mjUg%h6Z|ym~mFf{LDB# z<#J%43=X<%X>oqCeX<-syJuPH7ZMsuNAdLX>ihlu1I6EEfMz}T;`(Gi4rE_I-kA>y z5rP^Ul#^E?LcJbw-)5Y}xA<pN3RWE|b@qDV%7|4PR73E9da*uSQWpH!gFS_(UdAy3 z=UhAXwWQatHp{e-!nZZKsilR1x<W)Rq3&a;8|UiPHQCoMO^^2qHbe>3&W~5FiqsBF zPv6WXYQ%8&?%mU!IRciq@id~+Ip(F8v8X;IAtSTDf2Q+2MvYBHo6Fxld5P!eQ*cV+ z;_A#k&I@ZwyMqIK;i%|4dv2^NpMG(1@x_Z56zU4w=7hZt4q~*3wShh}KPDaz>QydG zvPB7~FflVTpFDX|2%dKIv2i`TUUVCPn9=9C`FUz<irUwX>|1f?e~+2!Co3N{ZgMrA z;oG`-^BvQ|Q!kpDmQf=3l~xbXA_sIiab~%_(He^(@#4ppc`7I!!*Nc)3|&R05^IB3 zQD5KDq@)~pf}2P>y?>JV`eoe#pv4t}>iqM+zsDvFx7-L!QS)ii9ZYEtaJi=)neO%! zMmEtGEa)nN4_;Fot_q+h=0-<3c<|tBhax^zPwD&mH)`Tr1Y=@j-&9=mQT3hWK>DtJ zc}vkSeIF-`oLOC6-PZ+6ORs{X?tXAEIo;5D<;s;TY;1+!lpBoMGk$z}Uc$w7X>o3t ziwj^hU+K*IC)K%?i>wsfg5r;dxmmWw#!uSg+Yxg4@$#7sXM4()Q)cIeQ~%qk)A8F3 zmm!)YBusm&DL8criC_2OVWu6vuU|d#_5mG5@_2DwS3b3LHOwjG%-8ot;<RYN+<ogc z{8WGRlt12A=mGnwvb^+{k<vEy!P0kvQQ70$W+`nvG?#KfKtR|VtK%hS<JZ+mS{qvN zuUQl5G1@^#DJw6}m?|N7`}hu<I+kBBBT5xt;K2un&6jp+bd`C@QSP{pv{C%8UE}bb zdZ{?Mky!KH9iBg}GV@I`jI3dF=ApcW+oU%rIPvjOfeQ1#HU8UZ>-SIWPq#nf9vd5@ z@Z|<3rG_Nl0Rm>STJf7e_sIy~Hc7{IRc-=&mZh$Yif2DktoUylmwCG6Ff@t}x286< zYAme?tgf<yP2|Tcyi2XC+k>;vNpO7rd#pQfcGmOhvuDdFW+i7M-xoQ$Ww11sBdi)S zGvk!(KR(TuAE(Vt4ZD1Qzayt^YDif+Mc#fjrRwF&11^{48hw@)rdIJtSwtiy@u`Pn zN8^2ceP0(j9<%p?9Tk{=5GnS4d_;^w>$3?ei%|3VZS|$)hOew}(4ddt#b4Kdb+s{7 znKROmpX(7>==ki?x8x>xGG-%HZx4p+G|vvk9!hV}iQcf+-TjF~=Wn<w^VWm6uO-}4 zbdF+TU-v?n!$v8vdRGHq(6wcmcC9JMi_0kd%I+b}7b|e({)iNS?7826qUS;_a+xV7 z?d;r?u=f_)k?VH6zJFkSCS6-c>yAYf8C=bQOzcJyYi!udEe`#6y?xg=p8ilQNfFw< zJqp+3WumPTR7VbGpxl8Gn4hgiC{|PTTSz+OCt2qfUO&|sV*A7_TfQcML(4POcQRCd zd7|f>=5di#5f?9BBq`5yyJ(1ojsT7C9yKEIT1?Dkl?y+9HU@F5fAi?5IMB&kgq-c2 zWw_I%Ti)RA*B0F67SW@fxtnBVSKGb1t!ZMyXE|8(&uk;s)^4rjv59thleV{%KW6<c zg)>@ksdIfbjN~Xd`1n|)q@*ZS-#?U;ntyN|YT_P|VXVJWJNmHJU0DDKX!c}`UhLLG z+A}{s+3!>qR%q(&we)t{zo~<bet_B#MMptVvgPIE^rPD<7gknRSD$`+pI!O<ScpZr zxAo*;qtTsNWqbK`^<F1(W3}(mS)H)r=jOhGGHtSvm4q``LY_@MtGak=(3LCJbCW~P zV}<k)u}Pd-A)+4N-qBG29R`3z&2zqAo8ePD^ZrP}ltX5_13uk{)U4T@AW8B(jFS4| zn(#h7y@BvyHs=D1f;v0_cKK)nABBUHGvM93BkHC$(tNKU91>(;VE7CZ@jag+&9`Fq zdr5EJr6oW0(ISJ#?eLj|oDTSaBv4UaiQ?5#Sj&*>OSi!_o;Hp}s*`@)2p9fXP@v3J ze}BqXcV+WRKLJru(XNV%&-<Sp$qlr~4YtT_c>CZ`-`H3L+z6!uNioRUSej_IF<aG6 z`lllO{r#_VPN;N!_%QJL?rsi-0IAD3REZPMxXiM7aPR*uu0LGrPUk46pkOp?FGXrn zH<Veezxu3=%|=-{IcsS?gS%RcPoF-05xz!?^)6zN)zp`ll!)v`=gPkd$NFDB_ZUrh zx9RAU({GNwr~1zRw8pmGg}pv?8(v8ORZENh-Wy|MPC>!JRD6@9rH!o}<s;sWt8?$I ze0y5nn30CFqCGUc_jzdO%FmzYMyd{PS5@VQ3DtaXu8c&TJk{;hzc4qIw8N2yV%1$> zOAO#xfBWh~UOtn9%TZ}=Q1cGLJxDcMTf5sdiDObbPV6WvYV?qZh{3`Z@3U6EGe1t8 zw6&%4R4|@Dy3=)#rQ+}J<2#j<tS9>G+uSbxo$f?5X7<D1ovZmYye1iD#(Q=2^;f)e z*g?6Y8OYS8&T*!vY%PhrPs%nk(e{RM^|_`T$}rkLx<TEyYw-09fkx{hnJh0(bDKRm zU2QkK+{ed9|CR!~#?peEajr$=mzTHh?T_8!o3EPj=Je0c)$|`LFIlehEB2mtSU}zg zYn9rzjRoElldxR)%HdtR{D0wr)*-IP54~sda-Mj+B8<)o_Pf*VOT?3pcQomrIQMQS za%jIC9lgrj-27TX!qv5Mr|!VR3U*8dD;i3f_Iq?%<FK8}r+HErv}~x2@Ig*)ZbnN> z%N&OEH>bmSrL?cGazFi8X88R48Q+D8hOQ^}Ybq8d^=_;A@QCQfeGVVWL=3E}ukTy@ zJ0B7m$w=`<QiM^r<?~V^HmI$$^qb3gqR-+bN3cE$xyo~!vp>4g58%?e(d!kQwlz1Z z03;oLaPUgCRaKeS6c^oW2<N6eYQsuZ&z}_F-EGH<vz%U*-80H00XvpA;8j!8Wl#~Q zystX6KN1!asv4s^0+>R$37v-FA051_j|%{O&t(lyPEKkiZa<Od*ty-j%u`0kD$6J{ z@9Zb#I3(XxzdxB9Tg{jP=?<l;?+slolHTY<xdUAHwYzX@T3XuA#rg5(ju$>E=f|p& zWT@#p=A6_OrlzJpfBgzdO%)`!Pmnw=)&zQLtA>UK&O+<KTj7lxmz8@@_lK9cOiqPc zPD?xF+erxtQ2^~^m2|UCHpUPLFn99S$w%sb$<r|+J$nQal9SIIjdqFNpqA%3VMF$^ z<_eqAvVj*b_K|IlT(78YU~mmfbtqc5E>fHGg^IFr|ASP&nxN%tIJmiOk6v>Q4PCR< z28ha67WT+5Aka5Hz6xuaotwLho%NN`da<5(53U%#rPuo`!-daPbQJ&aaJGeo1)Od^ z_NR_UOT$KdeSJ4@W9x?x>*4L{8&p00)6>)W1M+b;IVYayT^P5(8Q#;sA^Y&~gOw=w z7HV|I2ODEwk4~R{|3nnE7MJFP%6QqP?{(9oott2<fK5gfw-JtjXv4vRY*bQO3+9D| zm9^%n%YeZ>ZT8HMsCV+te>VfeQd3<`w_(GE)1RL00OodEu~+xdp%PENqK8;nSCJ#{ z;@=rdb+!*B&N{lftXEeFo-NjTH{xE(Iu?s6Zz$2SA3JBnh$`r~ev-82pWFDyAlq!m z*_U>R0zf9NWlOw<kK0N4_VLpvL;UKts+W=S9l~xsek=~E?sb`$ThP_3X{dlc@~{Xq zJT$nw3~0};!0;W<i`Ic(zkeqT4VHiYY&G_=bPe1^aV=LkA2m$dz|hG_LN{*P#7B$g z2sP%<&$AQPD=$|&n4%gA*dcG<#=tFZJQyyHGKQXwM`9OfoTdcHa9n-(sgd^CZv(vT zdL296zjBaZ5ER64B*%O$;Bf3KYef+m8BP#N+E!LVP4VK1b{<K_kK=Pb&23z}b}gwa z;JtviU*FtgMi2?UwDgzv;{5NUgHo0i{1j>TuLgUcy@>H%LdKc>@q8RM!-#B+IB*x+ z$|qwJB!8O}-VaYaS19=+moT<QX;E(V@CQ2o{`&CUpzJb?`cNIy>{2gk%lWRc39MT> zn0&BdNR`<q3Wfa^mSt=#ET0P}^#P@B2(gTz8t~<zM&x4|iP`3*F(Y8{yicA?f3p43 z)a0ZMqCAx;FhGXZg&NNkypIXOC_Y~5Hmqxlf~0|u>Vy))2^HZO>Z1r5nzH-XDh5p} z!~)t@c`7Otr4&mJhA(}6)n8guTfs2_WNpBun+&x%7&-vy&kxYp>lJ){k2Nl%r{}gW z&1iSHQCX?6hG9s(eRMhtHDSx!x3TpN4b2@^63WUq**B;rdHMLrX-fO`^!iRkrbU>a zKd->He0k%9MV-eg0p+^fG*m`1<l0|GMjBOBRZX(Zin^|{ub;JvL1~tU3?Nf((JH@= zee~0F#Wf58M{~??oG5j@A|fJ^zc-vGAGJd8h`aK6WI$}#t(`6#D@C<fuCEYKj$3}Y z%HiF^YP;?rzrTNEG&xK%bmM2UR7>nyH{hekpP!ecM#umeqoAuvkT9)bTD75No#NRR ztRhK>CChvIn-fxd+cUBFu%o#a$vYf7A~G^E_K&22i0LSCwj?XY_VI2vw<1>YPC)R| zf{f4XPvJ^2Z2Jv4JJ|~tX85OH!gsibERG0<H!!%0#p`<CPD*;gBWYHLoSE@t&DMi? zf2KzZ;ljH1X)P@+{Nun3<kAOI)g|NS#|m3fK1}gZBkJqx^RXuKASlR8w6$q^oz{<y zvu)VdE1bM+mBp(u`<ebcnC*(BIxA`E3l}c@Za-!@VOr{npW%TYAInYIsHUKh;N|UI z0COJh>P+aES9I<<>F4LC^gA1mjJlK@o8{i>wu_T#tzTiMPDp%Xz+f<!`-f6r;dycV z*PX9&*zDeN;AR~|b-~Wxr3cR2<l5ulp5874D}4^N!jXpt)nj`MKxEL6#ksP>kL6nI zMUj&+m5U@D0NC08F7t4b`bFiYIMG0_sV~-ux|E3Fn%f$eHVnT?uC2H@pJ!FQ%>FoP zxvSduH1XXx&o4OJ+hn(8=+RfX+4YwB%;d;>>772k4JBYGaI_Vi`k`f|y5HOoultXW zYvB;FTb`NZS<xZmXgGBjFrIkkUX2>^^*x<c^(|L#+_-`I89`rnIPLG>nO>tmD0$BT zuYmqAmRU<fMe+uj5eSN~#o(E0{pslc5fFSbHG^Zx&(D9aA;oRlp5@pfHd4gkd8M6u zR7n~{F;Rn9En`(hA+a$32N;mom6t9NPzf~i73Qp^rS<dEh2H&=FU#~2xAy@pdcu1A z{)}wBt?a?-pD*Hx3T@lr`zy4g_(iA3hQAbom2w%3$lPFXT&SaZ;dJIPO;tNreCX%o z<mKRCW}H(nxPU%Ds%<Bp$%B)+x>7*-4r-l{s3_wsXFI!KvQI5ZGFB0E(r4Yk0+857 zA}NWyA4*nX9Y^1>le!vg#8srGFI_*RDi<#I`pvV~hjG`U77uGql%j%qr}%?C*xwp| z>yIaRBBgH`K!cw<EiW-r7O>wW9m}{5?gNY_ae4RE71j52qNS;{74`yEo(r-Fp4gVv zJv}{L&pgBly-7#UgeC?R*D~scS2H8qfbOdK*;jl0+BHhm(@P!M$L%}>?stw(yj1M{ z42nhvq!ii1H#&mVwY8fX=Hr@Qy<(!aJla65+WD~p*>vB&eFhQ5CV(ivyvzHQ`}{qo z7ZF#kUB4di%wx14{2j=q>h3~2ne$^N;HK`Otiw+<ff=}JH)ns^D)lGAp(88w?(X0; zU?PgpfsUE9))0=#FClUZ!>5A6g4p6jfE^r?(JdIC9aBYv2BBJF+qRHa?;LoXZtnNT zxfM-sZrzTLuSZ^QVW{qs1(0!oL#|i%efrLA9{ggSakd%!ngnj{Q8`*yo^>rE&=}m$ zH(|59Jc(o#v;A!9;WDS3E^d&vq972?&CTTlcT|8pVWVCa$^%Z+vzIh`Vg+h5A-<pm zyM>ix!S5nusLn3u&Pn4-$r_g|#4f-;zl@GHqd@CIBFOj!4#0ltro)@>Z|((mt0LUm z*w~mLP~!`cd@@>1^HZ%F<XVXDV?E_QMt?xe-4NnCK7B7OE#3VMAX5`V^{gq|<@^$x z&YTlj*r_aJK?ymz!-k6pZc4vJg@b;=3hNO*5Jm?JowPulkxRnfe2r7UzLfo{ltPR; zUb6j`tSzew3WesTrvC8ZbNzM8jZ0lg0K28?C40Q^6#HYN3|1_Soqwmnqa_6IG|vtB z!QQY)ePk)r*uZ@+`FxB!Q3ezuRF9Oxlxct^NL7{Z<Zj&hgJW;d=ZKp&!lr;!UH{x= z;Hu5ZV<^2GL3s2RHH|)iX_G)3yXAn1mew+AOV=9lsD0~YPlg;p+3+pvV6w8&xGPGf zF2Dxz9OyL|DH)y}mCirNFMs-4+6FG4SAmx=Cz)j@WLt0k5sYjwX<Dk_{p<7=mdo#& zy_Xg*8fO}bd>J3l^_1**H&1d5G8_0ZQJoFc2$aD*Yu3~ryuGuUq1sb*Yr*+clRs-i zQ<q@8vl3b%k+YM{QslnpzrXiQFg>;V>#tw#BQ7^&ZBHtBHN1JlX<=az5*}Xo^=7q= z^CRHVQ!Po??}^+&fygI&l7%whkrW#nOUIXM{qW6cX|KtXgw$>uJaT{gf<%;#-BRzm zGrB+zlskyb=235eJOgp(WfY{ytR%*xXj7^Xf);V6O-UQr{ZuR6D^h3G;?}@Zv`^13 z<onE=-J_$kNl0jyOV3*b`|I)XSKw*lTh4Q~N;3Q7%EBrt$IX~h%AF4vntk~20eKHp zR2u0bNbNawYD>k^qR-RPQftJWa~Ccob|`uN6rv0$4cy;y-fLl2hBCk)f<6WS79$)h z0xqQaxP9aH?FS9j6;lO-gF==#eFbd9>(=^PwzpKcqL8Qf<KPDnqUu2fPSmc_5ZbwO z=dok^PQQD|Ns<PI_E-=>gAt_*XyZB%ycvI_#Kd&X9-Ml4^J{OV24z6Mf_<I*E^N(8 zLG=}sB$W%>DFbDz)x3Z00$o8k98%0#Mcjqk8MU4S_C&T4|0eQgdX&*VhVlQj0G$){ zyq5dQ*(lNu@3z&|)!jkjZL4!CaqeZZ8gn^;a77td5Z2VxJl$KNwr+>R=E!{n{!pl^ zPN5z{HE?cnaQ~4k6Hf7CId#ad1{J<ukE1n!7$OB!&fkImA>`$|x7<OC_>GN=i;GF6 zU%BrN3XQtrb9cyc$0uh$t)t}GHuHR)_LeYz%7H==wL(I%<C*d$Q2FSmll(ncI*tA1 zV9-=fJ+YvQVA$cSeP8=6r<POrtfj+#0q5d$RcW5!=`vO@%FGmBJXDz8-a*>)@e0S{ z8Xl=X7Wehnismizk5>P)@7ZG7PHI0;PP*^)opt;CZUliqVPIm?jg?j%UpAxaw!7Z> z)3YlmLGFPlZDW4W?vUYl%xnN0PQv95f?;c44aQm{oe+e85`fg7=_GVz+FWpT)q?ZF z0z`)BOE2}>HpNw=x<kL56T$%q1PC7sk1)DXxwN>Rc_sg~Q=b=mmljEXMsJRiewDpy z=bjwUT9lNRaj%$Z_tjOZc5ZIvu}cvN;y7HV-KZNc=Az^^j*0^GVhze@RLq^`2WnnC zHY<9xvFoO)muu8o?;*|1GK(iAB|6AlbbO<Ck!n99d;Of4s0N_@JUkqX?9vA>L06In z`dsGh$DQ^U9lwbf!q^FhL1fuK{|Bx@Pz(_1Mgeica#Ql$h7WEQ5xI*RI!VcGi*ABM zC=MQ^<Vw=LKszH{*~H=!A-)|uvaTzyU#H*{f-W$>$I-Ljq$fP>`|Co(LQ}!tUr$Qf z)}DzP$$-C=u66tJM#~59SA*FjV1d-6pfRiv5YL@ESB3Ru$aZwhC-^4#Pq>R${kdrK z(r)wl*FQ&)y<*n6vj{sU=w>Xl8Vh+TSs0MNuyAeorR-e6vkT)<GE^FANh8A7;#pkK z2&B+D+Q8xVAy|e@?QBYvibmDC9(EZL5^}ALZeVT$;pdTB-sD;D{`IY$4f%H&W%kdX zek8}ZrZCB<$!4R_x~X$Rn$sZYCVuwqw)e3aE$C`H8@BWLFkl;3XDNNe@RJpM(iBn! zv$MyyCEvcy)#}KeKl$<b1t~loMU#W8&@VhO@Y%(Ag|vJ3Afe#o;kmj>V}avPifT0> zN6>`p>h88)T$n>YEV1L|%Xy}V;d+pq0Bt9fO~Ar&wY`0N{pQUuGzR<M9@4l{SSUvh zitz{Nr}(rh#vgnC&d=x&pzEH`td+&Jcf4YLJ7vg@nF6vc!JC^!cn^OD_3<!Yc=o5< zfoIj|r=rGBlD1xk^mql;&RxLB(;ZoBA05kGxoXv_kpd!rsQm{FaQp(;)ZNC$JYl?2 zE35dfy^vC3+(tiuTZ%8+I8kYyZLBx#3RwF3q2Z>n-bz92c|DRJk<SSF&1$;r-wkV1 zLch1=w$fo`e=*a7%{<%9S$KJQGdm}-{S=`Sc27b5e*I7)MS@9Qo;0}(Dx6)Z>KBDp zk4jNsfcH1<^1Y#~KgLCBYSjNWR4OrL((NUJiJF=k1N4BwZ&=lZaE1w)+|KBZ(M$RX z&LG{i`dF3;9g5`bC!TG~VX-+=p8BD6GWkLe-A|__)8~h><|6b3HmSqtL3Yb@ccVT0 zx9+;YmmG<cVck_GJ>ML^Gq9KLK-46BD!15?Dqvi~wi4kCu;l62cj(@iI7gcmIqa7# zL-gWPcHf320<na{ze%~RjVB@N%R2F#2N$X;ux<W4IJo(!d+)`W6H&>B!JL>sa~IWF zmA!Lt7-Y@WgAZzP+8pcFu|l{b@7S@Ts?fM|nDID*eiccKn{=Y?02%Qqxo!q?5`}ZT z>)c!6i429LdzY^X1ept=+$JU_3M5VM<D*6ybQA#M#)%$u|00pO<(+%@NvjMt$N@qa zWTo}O>;&oh_$^H6fM!NH%LTXf*S>ds<(AZ$iU7OXCG!&}I5{}{5$yZG)v~a#>_SUJ z+U<)zVOL=^EX$Wyr|*w_oln&P;VDRJ9~4!Pap7@;04=G%zP<_jK0iBq2N_ps%iD7v z9(zk&he!eR@tFsky}f;soZU)5L9Wh5^dPGg=&4=?LI-E39!b-#-Mq;fSnZESku|yp zpndY(l@CIbMW{j)p5Vu-KYcp!=vceM4s1dI;F2(ymNi@StFEx|N;`E)r{BM?TYax< zUh<&BJCxE7kBI&H{(+ZRH`=CzRz%__6r0gSkyW?V)YJ@dVzHa*T@MS=H+k>>QP`>; zSW;5rJ|=hJhq>sk>8YvRSbF+$jry7G6$&u$5#?fEHAiRXRVpef?;amp1xD%EV0)J7 z8vpz^C$=kkm>_$6{`M^lEN^vlGaIr64S~52Tpki=70Nwv<1GKgMBXStjVMG2gKzE} z>$jsy*{b$nhxhNXOp`?v=3LDPko|Ty5)yuYY|I8dXqaQJG*b8kU~Ouxm!~IGfg3jx zdbq30ME~|qP725B)f>h4vv?h@YowEOV2De0&O#{yh_|yZXrX{UH^w5@4sqhtChZ6- zl+!k_32W#jtWz&7DyaaaMHP^Gp8)(V3n1>9)}!gF;?f_K=A^3|CST1w4yqvwK}14L z?ZkCIh^t2ah9@R{-fBkD1|ajxWR+vZrw$q#8jhhViMhWt^Xu11bVN@g5!xXL_JtRs zd?b|!SlC&KaQzh38>l^iow;KzLJA5CyP+yEfo++#KRRj(GRbXBg~oTh6kTNyP;_*3 zW3<Ti1k>wt1o5XX9|zDToI=}>G@2q^sGd!bxc@OcFRpKLGj|1G)7{#Pp0^x$uZ^2F zb@$Z<a}Vu7mR4E6^PEs>i~7=n+erEIr6pT+FixgMMyoC3IE?nO3##8<uJx+<Ro7*N zxc$?wMrV7!eJqtD>G)S=cjn=(#R%o3Po9A7<xTYc-EG%4`tgZ<>_%tgx0c@C)VV)@ zbQATswkkHgdUXngTEh0m0T5qIEBLC;etK5VN0ks34#0<Gu^A|LhHmW;Et!_Fl!{y{ zcXbDPRc005(~lbURxYB}%>oexegu3Df#iriO500LHzA&8%+#+}c7NEo{I=`i;AADY zt3~#2uYwROgm}gOK&pCj#{K(E#Wo8Y04Jib(P%|WWrR=9f;X_uknBv`%QjMU!{$Kw z5)vLth1N$$GHW35dbF+i_|tQDZEAKn&V**9Uaz*Yf*splzN4zmFKx9eO7AGAR?iBT zm?0q8DbrIo*dy4P0)Oq+fQ~5jj2q;EslVFpA52Ek=6PZerR^6MhNEKK$Df?$)Z4$` zFkK_sBDYm0TtP}sF80i)XYutk+N3E@smlNp_U?lob614SZ|I0dK5qk=1nY?qzh1I> z%^J&Za*<I{r%>O4jVfE&2i8j8!ZMb_$dfQ<%WFnU59Vbc4-2(dLlE+%aEfq&hck~z z$jZt#=r6TydbO;~cg|J4d9#c(&r&Z|q^GNEvo7+gcw-k(bVd~wop1@<61*olR`?dW zCT~r`c_dAMNwOYATM|^2L7#)o$Zv=ilmoYyqUu$%l7DAa#o`~w4uA^<NHP4{%^RDQ zTdbt_$=QYf=&fumcIviyV|S(rIKl+srwdieoJ4rz=wGz@3IU8=MJA&%FWO|{Uv)KI z5IMTwkd0`fLa#V^6|$w^<(KoTB}>Nm<kY?kQ}x%06`<{iASlYlwHooU7HNVy?Qx~? zR_u)%p=bt<_~^3;yI%bBt2*=W0}_Tnv`%(AA2B!QR|knlp-rvVix;bp&tnG<4eACt zWyh>UEK2EM!kodfO4rp?xDM$qK!Fi9RXtam02`%3xfu53*nw<g!sT|A`;^4dDiz>e zzB8XLv~f#I?Da_cuBWfh@Y6eI$3d3-2$P0mpu%;c`1=YE$f_Jj+!@PzZOZ(1O^{gn zLi5X)k?Z7HginPgzV(4}?CEXALmRA2T{A;Fvr%%Dt_%yXYgmKQuk_C)9I7;|{tC|d zGZM=_U0p3AqC|bmz{q$PS{Y9eO0(@2N&p%XCV3R#l?FVE%lz*%kd@s9Gxu|PdN0~6 zfPb}#H_PI_Nq^9xlMyb~%hGXs%OWnjp07Fg#Atv2#%3rUv?ew3+n&@r#!Qx{F<#ku z_o$JPQBGh5QcpiBkAc`TTA=C-OU|B9zX6Sf`pM$8W`2<x|Afw5@-{z3EqEFeLUsq! zefS|$9OtPd=yLTWBq?>qN!e$Hc4r<qu@239md4gP|E>;ts@~Or?q>*U^c00N@9T!} z8)2{37*3N?)`-joqR!IX#8fxQX;iGWfsc1HA|UB&pllaDV7>?Pjlms`uVh{wwUV%C zNx^qTx7Agd<aX|U-0oWHoFF3kaqcC0O|MZlqK|yLm54%m_zF35RsX54S=V_<-@bm` z`f%bNu@?a@gaJh^Cr`!FDAy5DCi{UZu~M=yeg5B1qz2imz-SiSR&2e=noJhjA-A5- zQGT6zD7mwAK|w)!Lop8@@8AZa1t0XtPmk?%veH*0YH-#9^j1dlvl(fwT$_msJhz99 zo_s7_D2AB^BgAE+uNslxbQIn<oVyZ`UtZq*PsCDT+q^qd^xuLlqzVnCriqGN^dTSh z9Ti3%#AV?1o+@EMyZlOq2$^2L2(<C{CZh9emtVw~bU0Z_ywW|eI}<<PlJ~XJzn4x# zFPAqR@`)JXCktSV=i>{PY{bHweIl|mKay|Ow8W&?T<?CE`0qkoM{I<~zJ6t)7-Sxq z98-+?-!j*X8tN@OS!kHc$9KFy23Ojw!9h=jH`Eto-?6a44+e)!jZ$y2M|da;P`>?J z-OrGKJn$km-8`m7ndD&yO+VT1-<peCIUm?c_KDSRTM>3iKJF$qH9rNC5-U>)M+Ipf zp9sm&0<yS;DwxeT5j?C`R^kISTE7ylUs?{=O25M~x)TtRQq0TuHtQXJ|Dw4$w#sD8 zk9?MO-6B8LB`z-ejup&`CEp_Dm9FH5%_g$jZ{a^1SIypiXO?Z8az<|3F7k1URVcdn zY!6#ukJIn;1si=ZvyyNgx&Qg!mm^G~&Cnt#6sZqn&{H7Vr9tLm^|+g-GXA%$Sm!qe z4p<Xod>0iG&@_``?st0RxJ;z5^Yx7-65V`owHUz=HJXi;lcO4KvMTBFKR=PRIpmV^ zi|@FbA^FeE8rXG8N;1{yaX`KZH_;txMPy~_ykz=skM?PbtV}-d;lXI-6YCtxtxS#h zcV--HriU+J>$BG;65ZJ#^TN<gr!xiO$9>drfqi~92P7PeH?x!F8W~Jr4as)m|BWPM z&FEe=3eO%PAJeQpRAK8FkiTPh6)wVIv$^A*R~yyoz>M7gt-aqP;*1hU(NHJZgRV5y z?=n2cTk8YKm;{bv>xb2&JO7=$7OP@mfXx3)x9;tgy5=3-u2%Y*P%3fQ+&Cs1ck_Sd zi~Z<lh%Hv=j$fEIAe$d#Lc96wE&QmEc!%i2x~ilty~HtCQ;xM!^@sm?!|7RZsV-L` zhvwW!a<+SIh*gcl!~XAZY@+i$qu_q2#lO9A&1(yr57}khcO!nnbz~vyUGe!SnFAGj z$?aNihNizvcyfyRZ=<SKWfbq=T0v}8s+IWDek9M}8_;?QGhXS!sSN+m)<ULFZ`(n( zg>508*GY2rNl_;hvq6Jfy_Cg^p33|$JT4eG9B13}pXb$iW0{S6+D^fo|9OW&w|8`Y z_5r4U_OvxOy`d{6lH6!6fRFEkLv;Ra$w~$MXC2Sbh!U;CU4txYj;@#L<hYIRpWWFV zfGfpxI1!b9<ElEhh<fE$F+4+^0sq;hMT$s%R(Fj291jTrVj9zY|I9ncMBB-#I6Xyi zJ>T)d|L@5854t+MBSimQ7+;)z-d&aS-x(Q%?#?_@6m{<|aj}r;EG5Y<r~Quq^MZwk z+w{!hEEWHoducXV(|l8M|9^*sqw6xJw^1fxoh83Ve0jtfkEFH40N5uMY^n-ar##aB z&o`N98~@v}&69WJH1sR>{%0x4s>>tltxqNVd%}E=$<XXQ{|(hJ2Jg6+dO*8;GLY%N zBkxl760mz5S^58u&b#Mi<HET`?tl0564+W$^epP%&<>74&7m<wI#y>H0)Y4b>}=mx ze(DF_b?d^QRE$5gJ&*Onyh!dVz#kVhLe+0v(`t^n>KrRFls)n<RPEE;D5!hpWmbId zZtGPM&L5u!&@-?6*~n;Y?NeF&4YUl=x1Rs;vC&SH^xq*R4hakEFYI{=9`rMG2$1Yu ziP|y!yfH>O`JlJ?>tzGT^;YOh`??1F8f;uc#J?yQeR+iER$qLu-{KlG&E^o|!SJXt zA3YV7BE&4}rBz~1<DJH5Fl_;Pv>u$^ti%=6gB!)fSfN||T-dV{%DY|>tz7{wIk$JZ zEQ9WCIF>CP93R>a>l39czZAY8+@&(nC!?H3a|Bf-RE2ey8QEqfHtz1VkKvuH&3fm0 zMMhe|V}qxraaT{Sq%K6^-eC~nm437%_1xF5ry<TlPcOLDZ$STVC8tr+kiC^tBW*IU z=3UGd<0n^!{=VOcMPqIKXsMwg)K@5iK@+0<W5480_vc`2nUzCQ0TRJ|3#v7=XP>`( zp>5?gnVb82ex!}DudmP7btQ<Lz5DjDpet5~7BnOZLKt5H$snM<I7?MOUU05Yi&W}J z>#gi}dbvP~r0or#Y=q48UwFuEe0Z$K)!vbrO{&>Sn(JnQX?}ZNUYX@_E|6B>PN5B} zLbJU!0sJ2m#h}RHs4bJ>opE`uqfXg5;t|z(x3%wy9$}s-{^ci`{B#7ktm~=ErqtWJ z(a(SiCl6h4_0dSc!8#x(4lb^s>OkgOe>FgSH30t-g8B{!Ro~(RP~OfJU%zs#>=z<0 zue^78#p?y@km}ujK9wNK!RXTJ!+Gbj_`!|_gXw*-#fO8Pi5?B^AgKQPV3MAIpK}{2 z{%DdDNjD+ViBi~(cc0tvt9ta|W(Rqja(nG<jt^=+=<2fBUq7eJ^?L&~+t^Vqpnud# zZ~Oupl~3JAR;N0}c$OuBd#IXKH#&Uyz$^Od#FtG?H2}re;^Rf6errQwQe!1;g_h{4 zKpSi+%GtYMZwWb*dS(rnAPmo7yr{;G)9AEe^n@Gu<Xf4(7CZ7+7hIWfY*f1IUA_S& zKn*G<JQA2K7xbTQIR*3-pILtvZUbuOu2F`ruL5PjPTuFY6U$|P{K)jzz{^(Xvjjn= zUpwR{d>j_C(0t^My`#qo_}h+k2ktsh3NvO#zkUrtsXf)mN*G7<mr0X^LbDivl9iAS z;G=e79%TZ(%~~tz&!~n$kXGeW8z>YIzI#x38{(WXpTGvBS?a)Y=_qQ|D=5LN&@zP5 zur+}Q+Ma^nM0w{>m?+zRTAuQ7B8fvu@7smg%CXBIsafp~q+zXIx@&c=&KWk%Jw(QJ zX=%|sIeL>q3rM?jN=l*SYLf*KGU0v<xtDb_w9sMy)|nd(@@TR<2Bql$>JQiw^{zwT z4!#xp4jee@Yg{__(P>2G04vjkMqS6c3|6YN!~>&D*7i#_7UjKgIO4I0Dts#EB_OYu z-6(Ygl5h~S(T5Uz=f9WC4n%8k*+<A=?1Kw;2!^Nx3aZyAfQh>|h(tn+LS}XfZ1o7Y z)Y~iA{`lCjxJGsw-*$_#5KwSJ2qN9x-AVHHMp1`j&=$J_4JVX7rq{Rs8F`RufQc@s z+i$nVoIG`^Khe@p6Rt^WE%gtlKt559=&asoN1|+ii{`k@b_q#)8~R|$GE2}$P`~~t z4ahfsTu-_ey1!8G5g{aO<NA#o%XM^gT5c(>N>=vJ0*eotgXnK5RUi(dz&Q<|EBpGi z{5FfSon%Pi@k-Xo8o1lFYu8Bg{KjAQj0X>>pqp{Q$OFfoc-31;3keI;ff<KfmB=2z z_guSuJ0dQQOPcm`qCt?<zzdI7|Ifgjvr2nMZrOa>!LtR>|DRwf0yQhqoxW&k*#n6o zwlJJO;M!>nWf1YDZsPVx(#uB4<$FDG5>-4YJPs+?qh(=&*6NRsrDQ-3v^_KSbk9gz z23^vn%GP^tM+jwVf4bl!&fewcXUzx!6&?uvL{tBogZ$Wwn35_HN@R+0QkEMq=Rl}o zGUfp)=lGl0M@E_Hp~b!zFA~#36#b0e+n&>>A0BxX2T?#jmP-miSR&W_7Bw{iRP;57 zCWPkiHrsXXTweb?*)jC031>OGFx6@+5*f8YEev0_!IE3qw}7G!dGbVF|EB!@>p5>n z(r5U=qr#GiR1U?nQ5?DhGQn4`_9Gr)LToo~LE4@~t?ECaLgq+iZJW+HihW0-LDRPe zRDdhPe1D#MMbtZoBJ|j6A%eHIvt!2YknS0TL5Fe8c_+9l8Hj=o@il0tiV#yI#aal$ zpjq%o?zrXjFmJGkV^TIt>1fkQEAwkU5$z5hDNmZdFhh0dpFcfgi-sZ#Aqg;@5e318 znJ>o(7y<Z<x~Z&VhlIrOV*Nj>>1i6R=pI!gNMLt=&dlgwm9MjpD=r{b6ck0O(QrBB z<UJtR3<^&jKy%R0aEl^zQz%9QK%|n$3&p1dA(ZRY<@r{0A)=w7!Eo%@vF`WUWJCuY zUZRYFgH3eDFtYIofp^u}dz|A6B*b9y+oYuoA*}-$$O&orrAwEv4`$a}o14ewMa%RV zlM{~I2D(^rbqJLv7xRpaj7$<b`8cLOSseaKZeQ-e;-U9s>hT&DpwO%<&}XQJEvCET z+-sm#aN8j(qKM{!GzKVOUJ5!@@o2Olw@{&1A>_2D=T2Ib09#=7&M9|ycX_lBf<T4Z zK>j}aIdGM0S?|Yl?(TaG3=E=rK7mw+;FC0jRaHOsS9P_wA9d+*#@Jd%mT92a(MNZ{ zGn*8gT;4x0(D(g2JKUUb@Ms+=UFCiL>{<Wk>Of*95I;>|$PNB5d}Z7Xoct`L5E(;g zA8(SE=Q{JDn32+wqIUdS{z_I>tz3(8gG#@PuqiQ}PlO>y{z3D0TN@p9>uZdAF<{Sp zXZwPEi#MZnM_RWqM(ziDlOaY09IJzTEC9LcE+|c-Zqn0fLd$|1q!SH3ZJ2MfmyD`@ zdd9tf|9%sQ*D$UUjzg`juV=*kRBrduJ)%zp9^8Q=LTh1sq8mYgfwFgZ4Ok{(?J!Fn zU3!1Cg6N1I2i^OO)*n#x)YOHpxt@};k^-f>3ejWTxg#|9Xv^Q9wW~nba#N_g2hf0W z8EF%_t$xwWTdnk>q*y$Ow%d+A;(`i01i|<oG<K)E3q-(*X-DwU6`4gsX!T{bhjZg< z6yn-JHx?8c=?BUl?s5t(HSU(PdqrBKs2w>N=#anUmBRC?t#)C$0Up3aY|FV9ns}qZ z>}aVN#J}Zhwh(3s-B+!bH+LWqtRlzdKceyCqY0Wdr(WG|+)7U&BwyC?60)^%X3yMe z+uOOJZS*Y|#8)AmCf~+j^*KyJkQqp8YwIv$^LX7W&~DI4SLC9JG5|CD+EcFLw>W24 zXz54eBY6a#$iU2e4?T>q1+=HB5W;trIH%fdlp@p`vOW<fwEZ6MjYgubfsSD?E6(jp z3mpYUoY3sGML~fF{4#B8s&86X@mJ`rW~Z-N&aM9cS^&%I7~=&BvVj5){j;*T)WqIc z-l1l=9SP*brje`A=c4h_Svgq|rFaioRU`@lMqkms7m%DRpsJ=uE(5dYS2^F3W^vK2 zcLYg;=+dAn(1zIF5K#foQQO^ZYPgaD+A|u?Lv+C>o}b_0b<UcVx&l3qDs(~ac8<Pz z=HUpHxpIRWY=#+%KTY)4iKv!zx3Ck8KPu%lVrN!|Z%S_w2&o`b|5s%9C3LnU3ZZcU z)Ib!v+R+;#q3VJ!F%!$e<^}@|TS49jU9cu%9)=ww0Tc;(*>)_4AA=7Qy_c4(tNiJb zAmsG^^|e~<o-MjA%oHWRC4ZUI78SlqL_SzmwTmQOz)x&6xsbK9b2P+IEvXuUWMm6p zvHRUMxYuv~yQ2DXtO;t^%LgA|$m}`h2Q<MuQ$B;#<C8zlK^nvGelqR-@OVj%&2t2t zMDOcflS`c?J>G2o`LAS#(X22s7(-BQRi9shmR&vR)1mf)t){{3v3h#IO7wpq4|o23 zsSHm1)N^8EkKg<mbv9(c=k9(JQ+`X(&`AQs5Y5NlXBeKx<ANJyZ@D4IjD$~6@&v>e zPMCIpD3rl!Ea30gd(n3wtcMQL{oA|cfTm;|4w7`d0Kf&4M}RUL<>c0^i=>^rH;))G z0ljB6j2^f2iPENgHWd(6P`|C@#ssFBjzUC<WF!J1DF_~71xF&jLts04G}dHDMDZ*? zSs;d7iK-fL=I7K@Ex;0y8+^XBG@sfykcB)*Mfd0D&!1HoBO^TP0*MnPX9c#KKlO(u z)Z~{L2=&XPLyovf<d+I(KZXE1rQtTfXR9%z%tDz(-onu3#!I3>gI3>E9C_p*@xP27 z2qa>d1061$DgvdPkG!$38L1=-5VdVSzPIAfH*wN@$Q(m}BqC=nTbR|>q2Vj|tB}ZJ zBeiSo@?q2YC|#v#d~ZkG(dj)8X&BL48DqKRZ=$}##NXk^?M?vI_~rd}hf+zkUW7SQ zz<;h1Tulx_e|&BVEi(Ao&@rDJs5lB>&-<Ez-mUc#B&;!sW`$-5GQ@8HKG`C(m*^X9 z+ZIyf+-o5wbpiY~4>xx`Y$H58oM`mW`<<xY45*fc$|tNf8%bykAm8W))R(xG+l32u zIQo<5UDzQZE0m5WNLjurD=UN8mnpR#=0*c}?gFGk;f#b5hjY-I_`CY$4Y}ab=g-A0 zoH5VPUo_Z0vsqkxv~;e27*d5M(ysJ8fV*S+WoUfpxMZPfa01<q7Dyg4u<ra65`g~W z_5$F9e4r>AAC*dru)Zvm^f9=#_R<4PQXa4LQ-d1)#l-t{@~5lN>Ea&EH7z8)EInIv zpfg@5ZWR{~zX<V@QVJDw&_*{<@&5*@mpyc-m4ApRA8}KW_{iw!#0}2f1y|7hI`O^} zdu;<T>=-P`Xl+ahvN1YnEw#0Qz|&zchJR7d=N=}Hg)UwqDqi*HyKtg7uwsmsi5^(^ z^y7Vzo^jh(Z2u`dBBo-lU|fhaEzw|Tfao*rBk~L`Dz$Lw75l`~KBNL)p3%1tR-qmG zsq#{)n(yp2d>Wh7$QzsFU0KS_$yp5)f}!c=E|`8J1|uc^2D4qR!Z~m?sFuC~%<I75 zAkW&hjSGMO-gxj}D|CS@YZ$6emLpi&@o;cdZ5@X82sd@Y)Q4}vAXEnl7Qlrc)>3J` zjzEkza&vQ!*qJ{)r}^kPIZ-5qZbak3Ti!WHM)}}l3Y{zFuC^M%t&;nTUI-|;24RtC z%F^H-K)t7se6tXXi5(f0LJ}hfu_C&2-Ds3`FD=e!{Dz*H{0s_5Xwfkggg&MTsD`!F zh`l;Gg<v)cwjN4>f8gaUPMC>m`SdASVCT7A?-j<;+rrWBYdB4W?U#k5;Rfen$vN^= zWbKpKWMU<Vz%bbGkPw5o(6BHY{1l){I{Dy(7u1&7(m(U_g($xuVce8~nXaIhNiu<G zb;{Btd?XP{&>vhV5BG3@wYsk$qbj)6EoBd}%C%aX`!CqRBcW;B^87aIq!JCzVx)J| zEL$5n5JezAKfg4zCPXZdpP!FIVk1L)K*rXnV4(Khg=f5ujys~-J**Sl1+sGkycyDY z2Uw>d&ve=(8b;^-{_%*DFqPFA&CtGAJ=1$B8qRHlt`*@wfLsWMCa(oKd-m+ULx&p3 z^tyE|6VZ^P<%R6k=+T<?5YhYe$EPx>5f~~__5Q^I2Rr)9;)^yAX~uqGkSx-e@5TI2 z4OEd(o%uttVAC5fVcIt6b(q36{0<9PP0_blrzL-B(bt~V%*rix2bhw8yJzRWhmrcR ze5PkNhEpI{s^$wM!&%32&z_GdKBPX!0HH#Q48J}k)6jPj;my?*YtvrDs$d3(fDvF+ zJ;|?W2*Wm*dwOV+zmW*bF(7I6?Oi5$E5kVmv*4+l4pnZ&NQlqbMf7riV!*o}Ui(k` zj(N%z>lIeu)gpYVvrAnnQ%`Re&qQB3n2Z^r{cl3AZRA|nvNk3+)3M_IxlOFczKBv| zgzsvJ%AOGA?L1c*dL-y+$chZjeXC#Et3T)%kDmH4?{nWMXLQTxUvIC>qK=;Lg{|-B zO<TRU_{`4F*8+)zg@wI%^-3FEP*j=05FZhcr=+Ar#)?Sq6F)8N6#Y}azo+5mwcBr@ z=<6!9TSd78C&2L79w;zfpm~^`owY(y2#h932JE3x5tf$bP*zqZNDA2FI>g`o{We7C zHT$y<63(2jsZ<wt_d19`tnBSW0Y`W3-ODI<s>SL;Hl6j;YKU3sD8<Fa1hkl%t|jyT z$Yunt&BDvx+}+Jg=I-%silEF)QDf+vv{;acu7EwO#=#E_4gLK6doeA>IIeZ~9toP| z>cC%;Ryl@~!-90$#vwTxPtCW#d-u-D(XqK$Mp{@L+M&G=QPYtTJ#tJ|Cr<{!zMvxF zgerdPQ}eTtXA3>{%F<>5SP9h=7a#uy@kkZ3laxo<`ji)oWktdg5-Ttv7V}a9lX{T% z?ERD_!qIdcF_{cTva+)7gp^G}Qc}jYiHjgSgcBQ=JXcp&C!0N2q3^$k7P^ha(U##@ zexI+eg-M&4fcu^@FZx}c-&-zZXJ<!(za?sK^4b|($=bnz3<eNxh+O+sszx-9#Ts=Z z)Q;Nl%I+y<Mn=sOCz3ttw@XPikU;{dYONvp8QZxZWL_U$Pyy+RHKqk(K9RQ>2)Uma zhYs!hSQp*)#BdNPg{I|lva+Bm8P=>>bL7Yo@>Y(qiHP<iT#&%CFf7A65xB>DD|PJc z$teWDKR%-ZhZ5E$QdlQ~P1P-Tttm9bR+*kBDNYNxwliMr5P&@)b7?S>hmRiBAPVO> z_o`E>kP&TPWq@-|%gf_{zLmH6TlU?%)n+dPo}FN;MVJ+ql8Oe}5dr{1_g))@<?`*F zsEMg*AF81-6V?pnZyS4U9rW4T<DV52#Js%9LebUL9lKl$xe?}fmQfT^icsm1*#wN_ zO^kKRadB}~H8w6M5}($_;VBtZY=eV?c;CysvnZ{FzUT2Vsm#Pytnk{cTg_tW|K8Mr zmp6p|RfR?<0Dw|_8#w0^TQdU5+C08{FD@uqlz9fgdNB50NYy`Zpl^8i*yzdSO-)VP z#KmhtMG^zx;^roh`>EH|mg2LY!cdooMh>6b=@I6U$n_*mI^#ALcJ9wF`>)@=)l^kc zNR@z4@p*KV4R5S+$ID2tlMTQT<TC_;gA0jy7Hq!#*r@|Hpi4+(1fmIsZ26=j<t|${ z2(Z`WS)AIJ1yDe%2i?56j*OrbFOA!jVP5g{>C<^^k8_m{@m(1I_J^)-I4nFo5UvGv z%&MbDj}k2%W}(O|Bjv7i&Z9?KC>!yr7i%~Qss{noB4mWa<K3_FMlE+L8;5!=g)k+k zU2l3kFDWXz5*f+vbmmOow{NU4BxsP+z!LAgv{=?Pb7AlWC%OL3n>S^-$*ZRDYO2*+ zwrsKK%|`GYJ-D3Z(;}tn)vJRN0ZG_!CnqN|1?cJNiMO5b`l<^8sbT`trdJbbi_q;1 z%jj;(+h2yJEA7#vM+c?cbw6c}=e(<me!wT!d*xws_SLIQ>KFe6UB0|5|3Gt1jaHUL z`8xCMyc&Oht$`HDA5vOU_a<TTK_$u>Jcw*>uSW(F8`?8k$P4pjmQT+GTzoJ&=Zm&L z!TPe8Iqp5zr@b6^W1jg=PEHgV|AB54?+sbMV#NylWTmaGt>~P=ahSlh)YMg8US7~> z0PrlMSW&5gM6!%(jZfWorwSVT31l8gOiawAa}vR^rm0C+U&h$L!2jyit0vr`s4t<? z)7-tAo`6>F0sI={nUQ~0TE5UtEn)NSJbJ{67k!wRnc0x>X~_4<8@n)Sjx2Nsfd1*F zC3VbXvV&(bmg&J5Dog~gE!b9+Uu|G&8VYgquKoKn-@lgKGLaY+wGxc5NquWw9Rmm> zEenecNQ%N7EM&HK#RgSAxI+VG#qQj@7YJJ{aMb?t+yTkC_{;}diVCKOgE;tAJvrcc zHQ>2~Sg(iNuzOXT)tOXhfsAfMJ#1t|S|SRA4*<%OAeW2cSE`}0%Drf8tj4?QvLcl% zkzcBZTuF+-WG!z(LW0cz-#1rDXlUq!%z2@Txy)|u^d^mwjGN?yLe<peX0R<rx}b3T zxYmuZ{D<{Vg{#X9_V&4!A|f+B<_fPFv%o-MzSKOz%tj3jLF}<Mrd?4Vh1|Z)&$oU( zfiP*omt<lRn!>_ZNoDA7z(-#*c9n9H_CQuzM$5w%FJb`y80T~fqP~Dfj~tS@J4uvB zC^X-m_$gom8Xu%b(AH*@@3~eh<egSOAwyu*AC{lu5IBDH=oLUY(5A}~oSckXV2pXo zOA*(C5C-yj)K>-v5;ZC;51Q(F5FA8{4u;Sg9Tv#Mt+)$fVpj93`|^P<J{jMRfL4te zXTqR@+cFdY*xO@h$O#%nq=@4X-rMKrrw-U!FIMkw@U^Mw-NKG9jl+K+9z-+=hr;GF zdLl#&{o%pGhcA8<%W3UYROB@?Gh<n?BIwa0G5Fh0z<Gzlk}yAKXvAR!DJ@;6m>QK{ zO6-BqR5;%730_J>nwbcE)nZAA6L{ZJ!2SDMNU;sf2Un%|ciFAEV(52cu8zDiB0p$z zOZYM4v|9j)_fVkOxOhT)9I#8sirR)^xN3L`Z;cD&m9ntvt+)s*n46!oWvJPT=VTCv zgG^5FHZifw%|rdDik&{76PJgV0g>^11qB6z!-sbRDdkvJMq&4zdMlDI&Ukxy{SyZx zGEYuT-Ob3jf|pPTZ`pG7Xw>3%B<!cYbF!pG2|wHgsDUwJ#^{FKxG^UhbO_glvcdV= zo3uKHv)Q5N)igK1eR8VzGDnTYV+he7bJ14;r4l_EB62u#hFwHt<dqvY))KB90-t>9 zT6Yf*@I-aAnjaYUL%x?*R1DrFv<s4Qe01VlyEqvKflikaTuVA604A?9^mr*XzFPrD z$fO?AN`96N(aaF}f`N&aJn8|turW>AxC2MuhqD2mIvZKA1*7?>V6}Jc+Eui{wdJNO ziXNLDO9->*g&8jV_-Jk05xh!JXzNyHM@PpSGWijg7x2yrVKhiFP^qJ-Nry@huf|w) z`0!y*Z|^YCSR6L8B)z=6JP~3eY8kJjVO$SE#Ocd#^oglJV_!RS1zChK_EejFyacf) z43x-j9UW#szo+N=mc2?*3kN^8O;q$Qm}mkISFp2pd+fKh6~WO5<IOze6-5N+xw{9I zlqjNw7RI?ryXxade!M(OT3XtwH`}9>=l^x}9pG5^@7uSiw3HDll$jkBO)8N{Wt8l3 zTV+%fDMTSei84y5j3P>Ap^z;krHqzBWi$|#`JR`a|NnU3_c@N=@q3=)zQ5nkxW;*% z*Llql6%{2f2l%kvEgy}S%<S6vXghfS{ylu^#YGo)-c)F){a_fj<W5EGU)lWpeCN1= zg7qjuC(G&~m?iuU%^8Gp?{{({T^D>(AFK3<>(q2TF_xSI@h`XlgsBjV@`K%jZ&O5q zpzOai(XYv#?@HOnWt%n?KQmmub?a6*_OiCN#o(g*Wo9bo<mTQ?NYKA4{_6<70+GWK zgny6<K&$H_h83b+G?Xq9+H_L(vtwFnY6uSX{lc?wsYYP%qPv#^{5+4VurvrME8gDg z`1EP2N2t4di>2jt^FOLy!*k)+h?dSSoRXf-W01qYgT$1mc@nK&6xk3uh6FN%9C1%i z4+<4QNRU-ej<KW8a49Cn1|_BEFJ9bfl#>w?V?&`F;F-V9gY`Ri?gSf(@K?`YUnA<u zElQfNEx+Cy9u`LS135B9=Ey&nq2Pbw#0h>SN1oAjvF}`P5;6nXF3wu5s%qiPcezGL zX5{g=cZGZiXqDs*#lEcWh?JAbe1j~A(j6mH)2)`4OVJmAe4!lV(WLkf@Q_x}ml4sN zoL^bMBOow$$&xt79YqR-AD3=AsSY>V;&Ey-1LqwoCLvPw2wB?N+9+xxHG!<GmDEQ- zd~LaIBuaY}CxWamJsXkeJ!xBE1Rn)20sf}FT{hs*Ki;ZTtVCkGK<!3aTBwx4Eh<zq zLl3~pV$qB{!_;DQHh=xb#=hne!iLflkqZ|tym{jW1HGQGGiq8|vydX`?%5N7NC83F zG8jjEDX9UW4fZhFyy;=EsLx-&u7q&QN3i0iPoMtCcTxzo31;u$O5>Au!o7nNria%G zAetj53V8#j4{k`Tvdd!FOySoD?d{LN1zIC_qQ1D<bLKQ6c)I+(%HH1I6S6O|va-4s z7UywFK%&^Xdv|ygH1_Oq3TC2}Bp8(gd^Sp?GqG|^yDD(L7jSa20Aa;pPeeq~et50t z;OBYL(g8JT?84N2j9aLPzDNq_5hW-nDn3E+U1P<LFbMk)?*kBTL~8j;GE4}|^t&K! zQ%8-|i1>|+jQq+zQso@>_Ej|VMPnn-Bo>4b)b${sa)O5tatKfW-kAj)BXx++K)<`A zD%|t$fVk-sGWT$6c3t&=3#R%Nw{2shQi^+x>rOHraD-yYAASuDRd5BN{>=*zXDdum zOCC@%#7!&=B!kmTO-*GX2(eLUK7jHkbktBOVU#Le2??2H-w;0!2g<}U6wie_Y4rOS zs&qFVJu*wr=>k2zbtk{x<sCP->+=b2zr}p3D_4mfz7P}RkiD2y?SUqa9Z_q@o9O8g z29JO6Z!AlfE>-sL$5)ZQ5p<NYL))G|*F^~UntxEw6jrjmuWv5ehe*&3!5UR4VN?O5 zF;H8f_H3rzA(ZLvxX(eyt0F?v3+N2nt9efXAeUI&gmPVi6|j(W{kaSr?GmgGadj0d zjP~v0!vz5G-#;RPlj0O8dpI7uVTmjQS7DE#VJY?k>7{~_64itmH{bLlRaSs~Ag2gG z84!(MRMbMRc;ty?b#-%r+yvgcC%?o=ke@#oIT$r#K)uH5SjZv`M*#bfeJTUCC;w7< zHv6$qb8~ZR7Z<s%^W{VhAmAL*1<nG6fq~QO>gy|bx{)-fHa9qnL`6j*E=_FnRJQlG zMn$VnZNUPQ)oTT)ou!<|rm~czD`PKT{s@Ef(7l%znP|m%^kO|hRRA&f+`Ocy&Z|O- zOg5C#=wOIRNF*Q@t8Qq}MCFl-i_7ZZK@MyJqXf}ZBV<wDJ~~chXx)ALG;vFT)Sf{c zl6h7SSO(1cd^tHeYFm8$`gPyfA_UjCT&zcq96_tw4jnBp`q!gkL7n?gUccsrc|n+= z39ixmV&8D;e9U(w#MI}5iK%J!Z<KS2E?m4=hGe_qvt(Cqne)|GcAA$58aleWL(pjQ zdYS~>Dq3JbE;_Yfn%eejyc;qsi_c({S1TzIJq#$8hTHaBSQJWpBqb&NHZSvg^l0N7 zG=<Bo6JV5=`bW!~&xUu2us9&L=|^!5;6lVf#^C~bNc_$qnG4M|HZ`>YZU}ygA5ON! zuB+82g0xn`O+hXJ9+DMgp9&aQ>h;M~G`pwp@+e*iCxdbV*rZHFq-o@^=@8+mvA|@F z;Qgz>h*m(|8)gLlG&_G840Hfu2@wSa-Oss-)FF(WP?B!&)9oIGG>ur?laGg~A9naR zaL^8z9tt2)a$ra50iPlYVI1W7DB)Al|1jR4_6pIhU4zQ;flkLY+Q^Hk5GB0z%zec| zWIJ$}ytRc1zy=UUOj~d;>s43l@$nHQMzB;RiYESWw3M@<r90~zMY_=2Ayp{?V2HS( zrM1P|dt>i_*&C;BydXLwjPqsF%y`19Mtg*~FP*7~arUnDUbT8psA5blMP~pCVNp4t zXo7a+&C3?F16@Gj1(6aw-5<Q7m4n0byr00e*O8#1+cxWNAsZVT+P|bEHk?|r2-h1N zj@?8cjij#O)~6Nn^7b~#2r)rX2|?tSZkEr5w^xHvrT`wL@tX*ps2Y!WE*O`96fTe% z!ZoXTdq2A5lqyMeXIxTOFdf!QRZg-Y3V;6URaD!lgXl>nU=4i?nI5ZFtrFODQh*}H zm|6|EYQpJ|f1N#NQ#pZ#K*X1HA>jJx<e<2itsRt@D2i4hlFdXSLG1yM0Vyz=_YtL) zWvHs76dq7ir0TTO>h06ZfrE%CIfaDI6ZUt{5dE#GuS@5!cLX46F@Er_hp^ILzkZPj z6X2$gz_J(t@$-P#Rl)y3Fyu#uGAzO&d;1w2L9CF&L#Al-)F;jwgtEg&j$E9rwf{vD z8ELuPR<u^nmX9G!Ls?)8&Q9GR6QB;l#V05Uw*Js^+m)7*LS)Dm^pj%Gi0A{zC;<^X z#ZRDci4AjP=jJZl@g7V8SERx_b-vDIV_SyX00uoQnSXq|5Q?XPfN-cZhNMOvIXvTL zXb#W~D2Z7CL&Mv*quz)!xb67yb-1yLhub&9(rNZBS`D8R9nA}%`V0~Z^cl?tU!gBI zZ-3)7lmvJ~dvANI#E#Z^o@cnf(VO5M+i%F!%E`&O<L>ntXpTi38gkC$DtC2uZZ<F= ziKQT{GF%&(FUZJEb98jn-Md!<Cpzg;69VPDItz2eIhnQ>d6azKA3BFuu3NdsPa!~a zrCBgyPz8nihVplTmJ)D^BPeXgVrON07ny5si0B;9E%P=qUH&xqPO!?zq&1xQNDds| z8lzuXd<iJ`7919;pDpxg{9=lKC&<HdqY%PSdAH8Vr}8yS#6~N>Fn9JoE>PW|Q}^KO z2=z`90i*Q;CDG1re}DP&Hn*us^JY`v`kG@R{!oJR|LIwJ^lxO>uA*bGo|Z1N;M=5A z2RM}UjEuJ9<69xcDI9RWYp&yH21+gEZ{J=WSCN$5{%OUY1|&^2IH8-o9wYX4m$v@7 zThGE`7r-a+IPkE&$Nog%cxk5ZT(;l>9)uI9#Y&VSxOjNPvX62J2SAVJ{IBot;*R#U zMATi|5Cb!F@muKGvmzUX!Xd%Mr54Z}5fh_{2WnAFpua>D?!{l<eFq)?{0|r4UEJ5+ zUSLw(E1{gi#m~PTS%Y}cMbI}Q!O7g`rK-lB56q++w18_*fBgLG?FP4uRB7wyXV-Q> zn?VO<1L<sA^_|`a<Gt@)zc3kPxEKwvA?MFa;BqIWr-uQI3kHWI3OFSvsB1N4s<ogx z1><05W@U*WdKX7T%^ZieZW0o@PN1O7Z%XyuS5$*!QPfq#RcmqM3Sh5w1wecbM^L-R z8^NiVtZW!sj{E^TM4>bjgShMhP{&Ny0QN5m$tO!MJ#T#ZQWRD=po%-X>CSHCbXi%a zq>8ttsvx20Yd}jbt&+exH+5UKWYrf4-m*}ysrfu(3n)9S;0_q0IgS3;3Y193pmz>% z6Xw8SATm%-NUBug2-LT19G!e|JL~D#-XNV&#SW7hs~-u*0fP?_KewRkNa4On1$l|O zT_y()%D3gY)PCRm0BIJEVIXp|Le_UlTHLAET5uwS(YcN*z~z(?0z6{7afY3q<y!@( zwo|X0nru7H@OO4~xdL-B`L-Ek&UV!F+6Vu>nwV!lv*1=Q<Iak`dj-FL|Bm?kp6h#p zMiGB%pdnkV@J?WDm$vw^5iZp=Fi7(mIBY;fxrm_vNPef|1Bj4RDn;ME!+{nN<d?;5 zKYjy3iQLI>jDP9gwlF}U>_JgcQLMOGbnU9GDA*{%vr08@GS+Q=`-64c`&Yl_PA&;n zwMp1ypxOYiJQHSKdFw`Q)e5nL2~C!;11$lU=beKy4c@DiddPd^*DRPCbtJ!l>gVIO zlKKry=+O7^?YK_`>GSNQD}h~)IFUz@7_;UQR6H09TW44GDzGPuM}K19v@b{Cq%Y52 z!98ed@abYP>yK6P#(ugrcSH5fK3JK><A=`yvk+BL5kx+V@R*mE_bb5hbNwbtdT)OG zcEX)xM}}i~z-OpuVcb@~fsT@DC6Q0s<{y+-rZC646yysKYiOdc7+DCsyzT2<$xSOr zf|LN&loWQ{rO0=ed>0_CMi@hI0@8PWlJW(q0e4i3BU!gdhH&^h&m;T&c!U8>t08`; zl!QS+G1jrw<ld!`nFbE|H@|Q8-2q87KTx#^>p=tE8c5sD-La!~h2hy^B@(8@?<xQ> z$IYYsT2AhqNyN#+MM0gWdlc_2fi)&AJ;Ep;go>bMuE5$0a_B}VrZ5~(6tP^z3MD5M z5M-=HGbUMbWO9T+XJzTzd$`W-^74zg=B_w!(opeUZOutGX&IT1?d^UDizu*JS^HhL zXFeP;X#yg~lxWrX`vl1fa`AGwYXxLcL<XOBs=YApvq8~Zz5H#x{A)I@6|goR0Xv3T zkV(Y~kf>OYUXL*3vM40d)z=rn2hAWt(LZh>IR?Z8`)rtcc%mEf1?f5l^K<!Y?I$P3 zsp2+`mDLqMIU~1LS?lj_WGg{H)Jcd5_|YA<@e(k!MZmkn)CH=@A`gHYYs|3RRCDvv zQg(@>DXOMnQkF<dDjj54bbI)K6FfxW;#fyWlWwYu{Jg2}T=HS1<_mtC)|8Z#5-{9= zke-2)M4TxZWpZ7AG8W}xkOK@|v_zhU`zI%hL){`1-J`IdMC_L@Ik{QA%fv^nS!{ML zzrjhduNy9_-5fJl=hkZ0D5~`yMXI807oR*fAF=A<$pJwsUtxE(lnqYmBD21~zfnZi zBxie8=z#xEm52JFYIcf&YBzNyb2axId|e@`^I7`_iaN8Iv+<{^x7$ipx^rIrz@^3> zRmDSp)O2cvoQm09W}R-#MLkLUk^TqHC?^yMHjf*G9T<?KpV7qzRXmg*7|%ZY@3ZI4 z(yREh<LNr@9cu-4J$|e_^+P?Ph;-8X(s!g>s*6Bt4E^F_=>)X_uWId`a+UwSEglzP zMdY=W3vKiUSJ6*M&YRVvy%Yb>qg|%4=ir1(sNy-rTlAk>H4F#t7O+1%G;eR16#aIp z%B0bM544yoL0dueM^9XZ5zifdgQ@5CDsmi<YrLrUz>ywj^`8A<|2^y+y?m|zzJHHD zu<fD(eFM8#`tD0Bwl>{2<@+mtBq@cd^Y3R4Y!Ch}WDxq_@5U70<u};(|9@i4il{hx zh0y+(_<tWXQvJJKTW)q#*?+I;n{LL9?Zks63->?H*KZbgWuJQ8P3tXl&A8o@|9izf zv+R{m_hu!I-Mz_cGqsG44g2tm|DJE&qHVe>c{~2!MX;1!V|R}&MMi0Afmeo!)Kxzm z>C+xho))k6_ci=Cg(FA5ZNI5IvA5`#pqPQ;t$R)MSO=;V&i4Pl5OqcEuZ1VF1j^9& z>=DC_I)i+iIr6I;g`h;+7=C2UWgw0no)|k`BR{(<Nc|QQ%{>2JrcAPQ>Tn!M=H1Dw z)}_$pb;@{?e#M^;;-IxRHa4O*cMBgu+Ptu+u27Gnk0|`Sqw7{(l;78yl!~bZn>UYk z-P`BumCTc*|IAQWz53GEze|yDr6)hU<;wH)cXdorsI7Em!t%svxDhxg^R)K$jMyvk z20s0`GzOIJS*Yn=biVgMp}_zYA@LJEn{ppF13o&Po2vv6UA(YoIsn`)s8fLFc?xNs zmPa<#bC8Mvhph@*v`VZ{>e5oK%Q7r_!aCDePOazS$g_%A{~l2oP-3Hs#r4YR>^}{x zL%rd_^7ZSlJdSVEed9I$v1}%UZiu84X+F6=V1e_Sym>HwK`6eVN+}-1U;s)a1dR|T z*jx8IgW5N}=X<BVy_CxTveWc`Hst5;8@Tm4vHDCzCyW4X2h<&qe5wb%l7a0l>2d?A zwybri9!*3~B1ARC;_UQeyyVv9;UxgrBIf559L_l0|4D-y5|A{h!EuNc8iQr2hl&X* z%eu+2FWOUU1-@u2Gxq$uS!HK9FEvG7(f?2lC<5+1A~JH?=KVD}_wSd$i?u3U2A+gC zP3DBr;~R*j-#^rok(6wQtBzfBDBhNDhs%##fP~OK@dLPoDmW{Hl2ueK7sI_V7^s00 zsCw(xt<sCWHh2q4<YMFEh#ejIP8OQWB=~_wsFIF;+JPBQVxHDFNLN~(V9xIP;Rd=O zGLm_)$&uz*rGW=2KX`4nx!zv+r>Ye86_NM?C((c#D0UOrt*lH1ZNgfVZx!hp8V>K} zK{M_NsFlDk5QYndn2gQ~4G9P-0n^pkN4LH2MFL$?SvdnJ57P$XZFtGk$OID*RN(W@ zQ2ijbZ8JN`0K)&CLi%_KB{YzIs2a6|4?`|c1)aNi_4f2@R7mjr{ap|p1LB3{RCc(s z<`4(PtMapxpZrYDGQi!j36mN+1rk#Ux<ODgzr%OXc#||(d0Vj8DBkJc-S8A-=s>7r zTA})IALhF6mTr&Wkh!9L%J<8o5mZ72r5x~MKa_m7z|6X_BNvlNG$N4~goaVaCvllc zo0oV$$SED(EDDW7u^MXC`yxbpDp78vO(1oZ#fujc+Z_;SwMFRWWsDdZvy)LVF;G8a z&|kq@0V!h;bWCfDuSFpS;#4(4gSk>bc5&03x|I4sY$Sp=PUZ8aCiVY#t>C#c78ES> z)$&HqwF;<r;&J^G<6cza12_j#(Xnga34O!KS=L&o|HjMQ(6CaXZRP6K6y6X|lm=!% zc)V||F$5O4u+^JE_EbDM=K1yO*ZV#QzDdcN*qaD(LItMN9%w5P*A_H;*a{p!(y#(r zNphGq2cuI`B-J%EAi22JnKkj&I;k3*?~g#AQ0)U8OK3XLK@kTr*gvEKPaREHT91hq zfbYw}U9_DLyfNHx5-A@BMF=Bi12~rB_(~hAUjp=x=*pWnGYJ3AyPceDjhF*_SL!=4 zxHLkv8~|boFgl`3eC_VG2D629c3b#WuZM%%%8l6#RJj7(#M;J&6(5}GdTAu2Zp~b{ zhNhXZ`M<%6T5Mw{IVk`+ID}wVZL@js!_*Upex6jO&R_WpIz@YzB)6NY_43a#2Kya4 zNc9unKYyOFd-v|tieA8(z*fWXlvysop#tOL;+l?m0eT0fq9rbY1Z*)_fsIf*2AFdr zCx-{y?>RZ4f|$wr;KB2_5NgX75X}R;An=;*$8v$w7KVZpusXyd$kAs&s2NQRB>*s} zri)OPDCdL&LAa+}IsldXwTIgos4o-9jR>5o-zep|>$CT&fhbB9IuK$=bTn%Tww-#e zNEj3$7J>SJ#QYNyL{L<6g+2;6h%CfTM`jDG((LYlJ~#R);qJ~DnW6yszQ|_|mKNM5 zyviP(%YOyXOrr*Z&1m7~WhG!D6ZGWeHEQyq7iC>y9SLyE#33gd7yR9(LKaBgoCe~3 zU$F*{Lc(GI_YumI4*6^5JJesHF|!CJ=lS#Jen6zZrZO1FK=rlx{eyyLBCNR&m5Unr zXmAVYW>i*IQh5JnB7faFfXW_y@P_?B1QKDl3JMA&K`a6+hhD%y0DLSA3b0|Gs44C% z<YBzhCE*ST^#U?RO$0Ru8M(g0VDrub(4N`Wiu`wB;lVTT4HO?PGR021g46=nqjl{% zc_pJf=SAc|J|2cn(|mxeWS&vuVFWCC0*U42U8(nv584wj4VH}6mu4`TZ-Od?VhJd2 zTLHnN#>-$p1|F2IcGOIOs4Y}km>f+GET8Mfy{1bejBKmqxK)!v3AqdGj4c=+YsWzc zW$4~rkfhtQr0!Evk!hH(E$mWDAkAeVoL^8-5Lj>K(7wyAxbRS&oQ4Sh3~rkh?hcW$ zz=nEq-iCq`q=8BA_o3j(@B^SAiV_^^_H4^?R$^U<InE%xa7+>a>0>rPkiLGLK!~d- zp|IHhVRC{GTnFn8y+s;ZaeHxqJ^MdR2S4i+C>^g0+<$%5BMMbXiJ#AhJE^I{Yu4=h zGjoCk@HYJXY$**E254THU{?_jli^2Adn;DVNAD&XQUc_W%UhwpjY#LXW<@Q`J}6b8 zu94}B!8+7~1u*<DOyznh&<9ev+1VDbvk`oa{s-jh0WjH5nwuZ&`fY-uFAgE2#9}75 zsca~~V<r|RU4J9S#sK)h5Agv=WMRO2X2&TbH^s4f-q;wCiUK;K4NP~CZ4E>IM(q-f zjhWlhlF*L`W0eUCXl8vvL%kM+zErIc+5w^Vhe{NfD4ZAt1L8;4A{uQ2*MAzg6G28Q zhLO13!eEIZB#1HInxEHDgo-HKXb^688ORc-!qNmHnkj)jyWhe1IAb$f3Bjpr1UobD z+Je&PZlpTAZ{(j6I}O4{cxURpM(1T9zMNec(V+yR9Un(72y5J!bHD)5>PL_gsQ8P( z4URMuT?CBMwLj3XKnyjabD-!zth>589h&d~+yZ5><d*T<-b+-|_zjYiJ}%(pHS9RZ z^y%=r8}w&n$VmsJZRg)v>rszA42ZMl^NB4mI7I3}HhOlJh!&)F7^s$l^@WtzEUWMd z!lmiZVFm0&xs>1&$<nZ}w1%_&;RRcKU^IsvaPee~v)JIri3axio_Q#^nPde~n50Vc z#Gj7^%iNFRdP&-Mg&!b^yOHMroGi;gT&eDw@idb)I|O~Kuip<J;5h1|{iT``+w zEqDXQ4;r!htP7rr0^QxScP}Z6Q7x0GoM4==GN=)bAt@&p2rdNLU8zF><qtCm?m@;` z4Mm)%wY9>t63xcBADtMQ&9Jhz7H%9Y9&Ip6hd*uvc!X|jaFu5H=~}-9CmBs8e(2r0 znUvHSDq9IxL#vJIMCQSF>#1{+wAjG2BGo)nJH)o98~hdD_V+j(g~aaoFg$G8)2Fjx z>EU=IQd?eswg@dK-=~T8JHU}F3;<N83kx@pqseyIjY1?SBr`zSe}W5Q<?I{=LSiXc z0f5JBin+DmN=5AI5&4|~NrXHB&PC`@NF-F1Q6i+4$)3V{->yP}e;c7=kk7-3Al2*o z`gsT|X#x&X;u{F{VB-n4MJS|+pM(Tz7<%)Ee_%LO2aFWt;?C=&$?Fz6OhSi@2o}(- z4#E9qB19zy8bXR$3_v6?u~j9YhXHLCAL<K2n-h9D?q_D60v2er)E_e-K$}Ch<&OgS z=3Toufa#Fk#g|WK^sTxHVh60g4cH9$fIy`FINYdo2P67e1}4qB`yr@FAz>Ij2A58> zZ$JoswOv_1_?|x4dW$J59gxa=dl)p4t>`Sh6sqZ?v1|dtmOX`DBX=d`<?p>z!42Mu z(C4nB0Q?`x3P#osO~X==Q>YLL834>d0^L~fRK+>=TLIp1q3E8jbZx+AKqX&)GKJ2s z-{F^~sOI@82tkXSQi><kp#wcj3JOR}049lroo^(}bh$7aHk>d1=cFD4&P)|pOJKi& z%Fxjat+|RSFO|lClAxWh8~}AAx(wi<)xf3Q%bXY++>Mm!3AP({t`go=5d!3tKFpXg zgZg_IBs<`DIO1M%a)n6<h`U?ZBeJrvuUb<6KvgDsmK*`{&n&);X*>WN8Gf*0D0&I) zcXZS~w=fwU;m?lX*6;x>Dnm#@N0@}W!M;yt&Z6KJ6_k*s$0@Vy`;e4T6B<ea<lCs- z4v_*?Tq~Q0EmmFM?be6zr{za@h~<ou0=XmkX4I_br;snn0AlosL<~7NDnSWwMT$&8 z4GnVu{_DlT0HHHNJ3SY^^%1J@2$1w|vdo18`ZLsQ*_dv;D&4Z!BCF8w$V${&Ae##k z0gG^{Ne!q2l-@muJ%p@<LElw?PAJh<Aub?hi*kq~BJsRKAI_tmXN63JSrx1w=dcOb z6*aLT02#d|CMI^D_btqQa8xgoXJ4a3F}4rHW7e433M_W32a+9|o8VP0S+XQ^nAA9i zHd-so0gMj=I2_6W;V65v)o*R^D>$*NPTiL1It6%0aS>L-uux0w@0S!y8J4G;W@j^O z9e=P4z2O3_5!cPkL3lLxIOx?ty^MNyirAas7`!oj=R=%V6uuwt@Qgv1F)jnVl-{j4 z?lOEMT@TdJwLFxJV!v$vHJa@0s}Rax*|Sxya%jH2tC_KJO~LwUr7Z@ZeQ$Zl4*C>% z{pKK46jJI$4}jG}X@Uem`ub7*+=k`H^&-kVHU4A&k(ls=K4uFTv_VfHoF<5Jet^(% zXlBftHxKYzc62vzfUUT<2>6Jf4rf783g{Qm)tm+YI&)c$#kA7DG_v8f$^$R}7y*h` z6LOqexljVdWwm;P>aFA|p+7sUWLvSD>2?a}iJ#(_5-;)}_*$K_KU4%XXB0PUJ}<_g zGDWg%AR1c39f5FPiRb0y;|l^wm2M;M#+mT&-MMi&SyuNZ@pu2Vjn3N4&pL;JyvLzx zT5fSOFaiXk3of$2W}`l6WB%oG{u~Ah7kv$NN{W-dK+PFoBF#c25gE`&v}|t!`52ao zx<nbJ$M-up5H-%@8Jagm*RJJ9xTh2yj3*;%>wTY5kIQk3b1z>6qSKd69fUwK)^6Bf zgIpT}lhlE-v<%kN)+VA9fN;k~u9z?*l8Ec<WD^PxLQtgr_=v`bBe}<iVWW^cRULFx z1zj2O(;2J`f*>}evEaF0$TgIak-=uNl6ET|5iKuNR8P8fiwVZn^X6uUNseVFRcMQm zE>D{_O?_K%nP&tzPFjz%|Kq~aj)0zT^Je-qmXY5-8qT<Yqtgz*MJX)e4?ROe!@FLa zlI_Kc{o6!V#j)n{uaQMwfmY&YCMtY#CTwc#NJ=RnU}a^^<lF6Ys^Lqs7W;&O!f90{ zrG<j)k6!4rmy(y?hQo(j#KQhAkVtySt$>eEJ(4s(P_!dV1O-Wu;ejbkBiI_J3(Piv z34ohgJo9{6VWIXPIFQ+|kmdQaorU~54qA3-(V4l|8>&z(Xo(aAH=B6P$isEb%;o}4 zL83FJ5Iy)q&|Yz`rFp8x8n)s+CB6;d6=4~kw6ySJKT6@r6s=3ykY*ud-@J3@6z>u- zD2*lq9aasD7}!!E@V32UIC(@YfL^%_seVEj;RzBwgwW_g3WC2LU85PsFRU~_EG$fc zm=b}3r)s-Ok@Lc0vN1@z7`~=Y<S{4`;F6H`H{snLLNn~G(M4sO+4qF(m%e@}%BU4; z`uuA?BS<9{3v4Dbu4uQQasBl4bSpc%K&0p-dX1tBC<#F5tB`oNfBSY0EJx-11vcjC z=aEHsAP_+LYGp^X9fg_*cg(Z?9x5|)LiGbPRm=01EMA-dgirtA9ryfZ<X2GkW8Q7U z*8AhMfpNpazBK~<xGYLaN<_8-GbIqEgC!XV5Dqca=2(NUL>&dESZAKD>VP7wAaDnN zgrn4+@sATf8|M|)`V?r6sPSYX`+zHW8kXn)8XzYH7}R9~c17enhU1#$i{^a&U)pG| z<Bl^*=nn$_!RX;(;a-;rh&T|pipcyYen~dHdezzw8XyR%@(4UaXiV5fp$x-@i3`G8 z?}O8WOn@esdZPA(kdSm<KvXZq?MLTN2}mfEq7rKfC6ZE*7h1&j%}sDCgs(@R-Bxg8 z5uvbx`bE?VdM&WbAR$igS_;M}dIr+P3t)&zZl5T?=wK%z{;~j)?WT5Q<OhHj<{^1Q zaK%_4-HZ6!dHXfu@vbF4DN@HOnD$06-9A?6O}M5o-}-5x6%A~Yq^AqP`AUR>J~3!O z+^z7)=;4MtwA4`KWoJ`ZwTfnI((t`seXRy=+s`SuYGY&(AX08M1;9QVz5nQ@t04En zuTZ;t!O`y1$R0$&tpr1fx#5N1edkjs2~%4SaNe$a2Bt`uNyr;<JUi_&qAoyPlFCFi z!o6}f3ll?%2BsrXM$+7EmIss|n;aP#A!>*1F@GDJ8KiaGUQ5xbfJs+n=p#YW1w%rT zof_v8njFM1^+3jb{PbT7@cU@(i7x=_55nMAv}=-W14$H7Nh!io+!IP?b#!!y{*Rqx zVIZ|777*IHKEgy1ClYLNN&{ezk%(~u1J_^VI}c3|Nl5`fa1jzR!R7&Ez?XJ=16IZG zcvenMIePzyK#h){9a1fGiHib+XOD+9m?6L)Qy=al#>@Jn>^LNwk$>wN83lu1PhCq4 zKLo=b_Y`KIK%S5FLUl73xeN~<Um{ACfq{X}AK(}%H$%bi6F44JU?j44XQwSSSlxf{ z;Gx_$&i~;81l_zTj0E5?u>QmOW+|zuH}NUhu=JJ7w1^On@eq}R6LCc2M7J7+L=C@T zc*s?pYWKA}t9mR$LvhurGQ{6VS56HKxGBHP#ilZDx+R1A3yfoIq&MT?$?^4cJmJ5w zuIC=FVVyB|GniVK>2(9Z1<w8HvXAYsrSQx2zMI)k0X#xnUt?kqHlCh|$!yTzQXkr! zL5fc-NF02nf?3V>yG;IXZYCNY0$YsqV4Ns##LJK_6wymTybDBgB@;+Rp9=FK&@(4p zBIJ<*1LL~y@E45Tf`C7Dzacn4lJEqHMyopw8HR4hIS@*%QPTX1$#R&^Oo<q97S;tP zxPfwo*1!7ySeg`5;zU79qyptqqkW#(RA6o`EiG065@|a;e!M@2N@*V$#DE15L8Ir7 zM6_GVkAfphTp_H;47@mwS?1&_*k1SvBgc8WQw0MSEKuu_-w`rbLML`JoYu#FPch8C z$QmvDg1co4l(DmTa(u<d!M{N3!Nw3<x9&3BaU?kYsG$5A8HuFkWt^7K+~N~MXVH8b z0X!r)w>b*ODhtR%mUHV$kWKb<C~X3XLtyP;KajsK#>Vmwd#zrzN)#wmKvhQsl8hZf z!>>}h+r*BedFL{86ad%HSp+j=3`AFJ*DiZ%H`#^Dp?*Gvnx*=_M2{i(*F>-RE&y&~ z^V6b%&6jrcSHr)F6+$EsIDI5|OPoUCnrP(KL7%A_UeFlH1t&Wj)8=<yYneq5Se^7m zc<oNW3(O8N;MyXHQo$<-LIk!gD-JnJB+xl7kPR{$-T?Zmxe3rTke{Fb0^q$G`O+_g zH8-~SSHqh*J<`PJ+U4McOgW`c80ZL)k|R)JSb)Ih)A#SYkdaqZde<Kx$Uk`ohuaP4 zQx>q1EYQj#FI^H_7a$J#Vp=8e@*o6Qja_69uM&mpTn^ZL89o|jLrY~aR~Th{PfyPa zpaowX8}kOfji(v-T)!kTHg-FRYT|{)Q2yG2yj%xVn~F9>UNs1`YY>-}w~1@*+$mm+ zX{YF$7ehuT3ef(dfExI2e^KnO1{;njMOwPL3lJql2n?HBJ2~yZ;w^&o1T7Vkp2)2e zdr??w{qhIAzsPs<;_)_E{a`p0Vf8;7J3oJp#N7&s$n|gdvTyX!xuu)p!LQl=nA_>Z zOl_=L>#<2BUNwjT*{?S&*U-=qLp=>%DU!%YAW)kQJYSr8=Z*-Xd?ML|@hhH&0m}OE z5~%%-$AeBI%^-liKttXOp!@X2p!P}uUHKFG>IkG#K6Q6XwaRV-82lGNtJcAT_iUSs z)pj<!0=~)o@#vP9Tn&8kc3faB82J!jakJ*k;qFDBojCOb`C_<cilGGx50NkrS8)Yn zvEd7F?OuQp@TsRq8#%Vjx~n(|5lDL@0r%C&E27ma7#U`rQdtZEz;qa4FtlKp5SR<Y z3U&g%v_+-T79IAT2*V{nuOPNkCqnzaO#H_y)!m5dm*ZC0!Zvrpo?DK$u8@+7IQ|1- z3|bH#s74E_^y~QR*LS0kxdj34iX-DfOPB6OE20(xlr6x@w}KiP0zueL1%1LQc5x;b zjBfe69uABfHh`WA9$LIGP2(05NR#KV#tAgb)_ni57>yK)TL@uxLwih(wb<IVu^2#q zPVja{#%_Rx;z7{O?8ZbF7-&)00_HcgB+%`}&AEv7tvnSmu>kbeWv+M&Xq9>3fEKBT zdCV0q4YJ1}O9Il#Y~F}?-Xkm{V+Ap{K%)_ZG&U_QZYh}{T;*!Kb!)K?CePzhw<Fc{ z8fz9474_#|yf_k}-wqEvMQgF|rkbZup;$^s-f-=)o~96pY=K9&@dY}Y4O!fe9u-5p z%dMhuw*^1DNXhY1OEK=@1>6B{d>5$o#>*@EaIf$JoPf6T<e;4H>E(41W(JH1lmmtF zza-?P2wJ#>e)z6%*TellV8>C&Zwz1u_aG@HWjnKTGB`YZIhqOjerLcQUA=bASQz|O zEu>GHxH4p2u<P?d+MQL?&=8|vH%qt^3Jt;txucvRFiIwX_5^5>c%c!fbH?J@NV~XJ zg3z7$_i0CeIa@>XRdkg&5Qd0KP<V?Hlse+auOht{LpC4=TTJ}7B;Uyi+6_4N2sod} zYu9!n2dP%ghK>rS>93|}lr$sYF|(Sf0&_Vo97oj_K`OJ^^xbJpuSv!)Ye`i;dGZ8& zH|d|RFlJ;MV*c`pU3Baq#?YcjJy21ajlOU(WFQxQhj$Z0Itx`>Btx$5?&46yTo%C2 zR|}|896*Rw!|0(^D0!52%;Gz~QRqi@Xx*WC*&&r3<1J_ViP2M7Bu$SFtrPM!k>>7* z%{1%oFOw=nr2R}4<Oo!7vO-{l!ARz2!VF=7YtYnC4SH)0QcHTUYW(bWYwMd$pB^Df zBC7~wDm2$1pt_(r3*dgF$#2D$wzf!c<hLvPE(91^158>hE*b1rQB-&%>n+1JFGGkc zua3?Ta4~a(zm9!>ev=g?&4`WZ&%DzdHi0Yn)AP}alM+%xaR5SA=!e;jS_W@ktm|w5 ze6zPI1K*5&?m*^+=y5%AQ=>K85oC~-5g25|?#5}LL=n!U6f>)3cfm4FVTd5#9+Bf> zXG1d07F0&MNQ6Lwo|@IaEMNgat%U1<e+=h=@IlV7zyaChT7bmpYPk>k5bBV#F}^?p zh6XtL&5Vq2Fm2(bKLNZYJPUL=xT1cHkot)8!wh_RIhLcb)70`JY-u8%d#AnVQ+QxF zP7W%CJbT6g+qez!AoYR3C?JxvwY8<X3?Pa#h@!EA#VLLMurWp(IhS3n0#kn*z-7>K z)7(`er+4hCEB`q5yLhq!z5&4&wX}7>sW2C@UmmS1zYtv7<<{DQc`WJaZQ%h6!2UYp zo=k=X5<DR|rPNUeT2mm}jM4rJ5j}<)JN{~xsA!_&rVea=3!8od^#|b}ew7nBMk4dH zfY<!&@6Z&8(z9%UegV#*A08C!&uK6+Kyv->&Y$i5?)U&|^ovIUyi>C|d||@Fhy2+0 z`?<Ll2p2OEtPn9A!u%ATqOwPWO<=b^;xG4u$&P~M;qPzPp-38Bp_i7H*8F!JqTN!w zTWhBhupV_sM`@A~!j;S%l1fZWBpw`TMFElc+L9>>DFT1kdQ703hT=b$ikwC9$$2g= zF3KI^Ww4?wdn{v6B_AdEAqXr`c?jA*1jztz(!e<b;vay+o^{X#5GK*~knHqB$(95% z0LM_tmQq(h^F+r6SAY}=sbcyTkT8N|KO}0f57QY4+cOmP>K-H%73zn+xX^jn1uA&~ zFgwU+#c?T7-T})pS?5@5!3!XjB*%pGuN2RSfoRi_{sJ>gq!#TL$+dh9s~QW^<^op% zJblr5LKaH`==-wms;<ch_Y6A}PVcx&?E`(gR&`7XpjT$g`h@kdvE3Pg=-<FV7)6YK zxe7TsgvYE5m~hHy>*~_mM)mlLUS<0NR|Tk}FT3&di~A|rOOzZrZQfpB8$i9J<vOMU zPDeURTmdjNDj|PElVHrCKu2>fsr*jC)IugyH&Av1I93La+^Vq`rxmUSZP#V-OBUD* z*VWdRfhmAyd^Qx05uKtsk@*_|DmMvapuB?GMGM;;Y(D`tl>Q(-DtrH42v7wbe$<oD zrjw?wnCtfK+cyxVXQJ*-^z?si{io1V<+ZbpEh71X_q^Cn3!Q;kda-o@I(cQ7^atSp zD}XQQ({3jw5+am$%M_mHoU80B8rcLSkn%v_1z;tXkQEkN1P}?wel=X#a@Qnh^@Sb4 zM<}5O*#5OCl^d;+VF+SM0Iz<M?A8a1{2j^~AF`4~aGPi{AypsI{fn{`4VyQj$RrS$ z9`4}`P$@}Z5ZEAEpD*UN0<6LvrqZ`-$2J6+v@~pNU;>2H+dusY<_9cuDVp11NgSpp zA?Y+4Za_N|;u1@nO$;i5!Mw*p2mpUFA4>~<HN)z<sQp3WNYP(GS&MoIKr%r0@OJSY zB+l+9&(l<uxk)r$3ol6)1dxX{j)!x78SsNGI7&F6W5vVYH?BV`J-+16gBaNhY^7Y& zPO-7?P`8b;o2JgGD9hE#ry;<@qT*k+l3nB{_XUk;#)-|=MyInfSVdX=t^9&#PdlB( zcHzEy>#VJQ7gUBO8h^;$IaF}*=*x(nXTOi^+O_Z97sKSpqqlofy?QN%DnnvlIHA9c zlz^dWbn82U=E9yIJ07}q%_iv@D3p;rfT7~Y&=p10o2)wi%V4NAiv-!I&HqS)%FOo0 zU%WKu4f$A;-UeLiDP|H#=+uj0;XCdQw2-6|AafbF{)aotRn(q{ygw!J;RFQB3UtE0 zsc?@Ztv#aldo?^Y65#@~K7GP=;1XQUFa4Mfqw&?Fk7MKr>cAZ^0&ba_80G9yv?S-A z;oIxuSY<0bxaVN~8u#DD*W+d59?0j`+<&<LCmZw6F$6;HYy~9iN75R|XbAvDC`-eq z5iU5Y5#Z!rR7j;Pz(>)7rEU&$Je#ht8%4gt7&hohG&|;$z;)3Gv^~rDuA@Jb1c;%_ z*?#xIdlH+a(kLT(R=tn6H^_d$h{<S<AedPz2>-M$JCk}OKI8!B1l$KH-B6N@>N=~h z@1*zSp7IZSbBtJ`4buU5-08iy>~ka!F@W#^ES{^C0%GSk8p@<DyY=y~I(pctCJ0%G zv_WC$_wCu~4Mj8XSuichF8fszZ$u76xaJ@V)4<A+>+}BG6{8SW52|o@PGF#i-(46d zk}=&GX4_UkbQ|<XF?ywzVp!b0^8@PuZStSnHssTd33))iX$~$wNFE@#{Dg|-M<`L3 z1FXEO`VuKr>?Yr$R{|06(G?iW<fk?1t2@UOrzZ%<`X<=^j=i^{x1SvS)otV6{1AnA zAifU^<R5<W(Ki}{y&~casy0Db*97E~RA5G{+gZr3J^KCLi;=!b6Noqh|5dtq&c?Wq zpGO5)q8lPN`n4f<|KUBj%kjWuG!W70R{7o3{2_VvH1*1&&`OPpz*w7-*2>Av1EXe! z)$tWTxbBRy$WHp_O!_ivM@8X`mwNv^^)8;3q5`P%rq3`_ysQ(u)-|DRL^dy1DQY^y zePWwSZ8e5Fks<<22_YXOa)|7WglS^y?a@eRad%WKBviQHck>}W&9?HEiwAW#Bp^eF z>h6UTtSzr-b{pcx{G{C2?P>5*c(~d%`&KU8>FPT?Ih5gx!JBc{eV*P{7q4WN4k6)i zKe)DCZ;m5EPr&2t9UX}h%kYRax*7lB51tQnEtep}G+g0^!89ZpL6kTWG^98T;gBGh zAzR?r(oJC5-E@<+7b>NqeZ2&AKq^<}%6x~Dp%!fPc(B2Rm#z8W?T-{70cavYq{N3n zzLYk~GuQxuGLn#E$ntqMY#a<*e=KepmyiqJ2cW4mCE8FcGP~#X-8l>+47D8DpaO!6 zh1WMFrF<Q9-obs~+zg2Gu1swF`g6<$Bfr(a{-X{u9AKM|rGeDFMFc+LnZh7X%_)Z9 zgNP-2D4PmcYETFziDs(L;WeoSg7iN0Rs9SUupsUF^iJA08i<VAbc*Pr*`E$2+}!{y zU+FLB4`T)rq~Z~;`uh671W7~gS04(G%||mqIRIzUFGjd3f|h)E*F0?<5M`&vUIWTw z$gc4s91Lu{X}h_W`uyUgVUWO;hR$4we)uu_<oF5H<P6TLe!BBp%i6W%#g6j*tC@JK zKWMO{HV`EGONd4TCPY)b5YwU>UX0l5JK5pl15m;B{y;j&Xch07e2Tk>^F0Gl<byl6 zlgCXRZ)<Xg7-Q#23Df_{jbZ%-N3twH6-Dx>2`&VpAV;NKEl3SwdSoy(S=w=d_*dD_ zC2dsjQm7l#QN<W-g2Z|O0ADQ}wf&jP&PF#dr|=Z)U>g~VKK|0#X@$HpWlt&>f<awO z^o2#3i*9XDBlddD-FU!_Df>I!^F<4DE*hvfF-)L`XU#o{&&nquMW4*x*ulAKM{wMg z&e%^AeVAXsi3;Ukp)Dc}AN$rdeU9>5?vebgCbKRCXMzNHXCt4fq()NQQSYHw`yk4r zhS2|K>6{hqfLw^uK;=SkG>Dmlwx0^fPn;upPof{fs=KzAu;8wd{@LW{hsmF>O%DmL zUvKhl#L-FhCG9<5+=|D)BHJ{w(Fmom_#7%D`peHXK(L|JnV4^iNX`*TseU*j-rd~| z7Dr9<crN6vkZU~L`$;T*%{n9<jM|)?2wrIx^0&N)>!{R1fhH=})Ib6xoyJi^B&TTP z$icQ8t(_;G&7Ga1<jqa4UCc@$DQ}>d)UQq{5n=ei3NFc+zCKuMY^^Sn(YU|-_Vtrz z@w#c5;E+NMVuNK&D)1EwLH{)*mVqve*2$=hIjLi7IOVP~pKW*Tpt=IIdTy*=il}t_ zhU8h3AGqK&o=Fk>DN;v52uOto1QbE|V3OhLS#=wq2r-OOU_{|`#85Z^)JhWs`&x11 zD3qVC`iB9-8(qeb{qY3{{v+@j3gt;{29CLNN2K|Xs+25;k%=EmZG!-=EOhynQVk!L zn?~#9#{aoW0?HVP9%l0X#uir8=~4}}*ioM)nI7b55f?A|!_bIe{@t}7E5TZzOhR2f zU<8LCZ68gfLeb3{R>1SiwPOlCBS*uQtl?lz)Z#2}(c?M#t!*LF*-FG0K?wVZi$+2d zhWE@;hebj_7(o<4(z`eq#J)vU+|ljcwez*F`{{M<v+WjNK@Tf+&QEPKUq2R`gzr(x z(Zo_V#VG@t(8Tayn8=^dYBlzefSRTsm5iK~i3&!OStq{$!V{K~(jpsQZgjQdS2VxV z@u$aI^iVa1j9-Gm<Dj$5fAy5DMn<s$A}3^V#c*)kv=X;!?YVk=9LlHA$%xQ$oz-5K zpZ#3t$>nQ35&Qohgtr6Meui8bMM+`XMB}}_uOk8`#tD%AtShvQAXFoW+eds!sBMQL z7Gn@SDggOzM`E}tO}{140m9NL{=)IHR^X?m1~AT2wdPnI5TAqIRdmT?QYs095S#dM zBFR?+&ZdUB6yt0ONW-Ua%)RBa%ZL}eZ@|rI4je4Cej7G;^lb+Bv~6S$CK`i+WP__j z%}!v@uwJYQKxvQVzS+ajETA&Tqf?Fufe!x(3aXS#z;mU4-l_xL{2E&-+h(HvMWis? zuu`C1gvQNXw2}=LsvO%eHDduw4!c!`|BQ%>b9&IF3?@3LIpz4oyC-9Om0|KxPw2o_ z)gKd}hG^1gM_O$>ul+d~z9iO#iR&e>lvI559qU+X$hww=QXDJgI7P_l(oAG(2!{;k zl<ax9NjAkBu<ev0P$-1IL6J8Yrh(<Yj;!V56YfNZBdY+s4%7=0!KaL_B=KM-@vczI z2kk2pNcUAtNI`4D)8<@b64FA5blXGkK^4l$AU8h2=ij%lUkq4P7zIba<h|L@aUw&3 z8G@f8r~Jr(z#t+gMce`mWh?nENUxm;?P8E^GNli}L|##Lxss9QhVYb>g=wZWqB>e? zTnvBgB<ua7alEYE-!L%-wfxPv9S{KxfOq6VAXo-=AqxXusto9V)2RVfOg*9DZ<G&X zffUf;I`nuiWH#}L)&S3lmP@w*hJi{tv0yxcv~U4o^Pydard2NiM$&r2+mNyZBp4V* zLQ2O!Uv|JO=6#R)E0<8A7yT8XD3^?((+<W+4d?^uo|DXiV9G6Aa*P?Mz?raKd70{< zu{siIuRHfy`Ya?+z{KRrj)Rpd9R0`JS5=NIL`03yZk>-owgT)+!D!*dFSs<xTh|PC zM|>LVn+a~HvvOMaw1_WUU-W|9j4RF{&`K@KXt=(~lA};uacmTBmPh0G>J=t=tS^W6 zt9bq_`K&MwC#>!1`EUS2n@@#<f>Wa~tT_cds9h)|>_xR93YS+BC1-u`rY-^RsE%9c z2otXjBlZF_H|)T8?D5V3V3~7;D@D>0G0|S|`w{{Ijc+9{FyLE=#W~Hz$G4o%6vg6w z6NA?$*RF10y5iGw*9}D^7~Pt-9!ht6TM_nOLZvGiQR7q8OQVoZragR3TXp;#N-r6^ zjwec<9jIA;T@uEn$LkxSr>D39O%Dw{Jo2(or%wi1S{q<=6l9vEAV8=Gy*jHS(pPXb z#YIHajL|tRkTuZia9s*^lB3=fKW=UZ#QrJPKdRZnYv+M?1-UOU&_i5NQSY9~jnena z11nHmjRKe{jqiMl;PVpT7fG}qJjD&AGMF9YB(<?Pm%xmxjpLOB!rA?yxL6;zCK)i- z-_LxzF~AZ4dlW9J#X7gn-~`F&?hPR2-+7&nl7GbQ=@Ee2TYZ*38XKtnduxA|`$iK` zPxlm!4eXfuaMaB&p_5+{#h{opDWuiYpFK9x)j4#AL+}o*@pBY6ve&tMxR?Vv?U3&` z#I0Ql`6XD**;hrc25)?cnD(mbWTY1b1PwRQYjL5>qxI3kjvD`y!G&ep5*zQxBq#Re zdQgcGZ-%7owt(AqFio1ihZBPn*8n4Wn~H;KY^8r5*Y!fPawwcwiS0^bcR_Dd^gyuD z%dk*s!Yo^m4#}7zg+2qgMg+Yk|N1u{6!^!)#f3h6xOUH7lUJYHHx7fW1WLEIeBP(t zUZ+<*Lt+OCo1Z$rbDxWrArij@iz=?6!|@!y;<O)+26vq@P!Iln_wjE*s{<?u6tAy; zyvrKOFd#4ut7+N@Dp)Zre1`U>ea{M%!bk~XKMM-V1qtZ4j3(WG2<a?>+mMb@dQ4T- zux*L2T4o_)^C%2@W3%$|eaBbQFyAd>g;6Uk-#ea_-ZFYQyIIYW2Z2S;T6PmajwNvB zcV~>V_AL#knF>uAmP-#k>OUiprEer-Z|$nWIa#G1dj7o$$Gh%h-S;|M#e!38Qw(mO zH&bW3!ET<aV)ygzCMSy=5NmD#fiWDMxgiai22pD0*^qRLBJRU@-7Pr5jhMd(D5g4& zC*<y&9*FR^qXOyXdQ(S&n<U)xW3BS<%gIXdI*acsNemc6T*e`OOJlE*{@jg={c6nj zrs%SOa9asC{u3b3CmabH#*A?DInw{C6VIyOf2fbf2Ho-ZUKoQXV%$F(bNe)WyH~c} z?pS_qPR?~zt=lZ(LNYxZ;v@N4qrc2XrXS`n7s6ohs%oik*(1zXSceg@ltdsnd@6jK zz7y6dDl|1<!ArMyXfQP5>WJ*rYLGKQjN@)q^oc6QNMN(X)7d*6!iJRpL}xE|7ZFK_ zeAYU6t6ql1CB}R=7uvAEaNv0(d5U`6z%Dd(s<tclE^6!csa639z6>la=rUXsa0@{^ z)DEXQHff;lHYfAZMA89nn>?YZXOFo0+MYRg<9bdn{ZLn2AmTouwr0YSUyg8t2FJM1 z7D}kvi+!Z%3#1DUpZi+A<yT89lrqG>*t&;}g=NN@hJm-6^7ZOBce_FD5`#khV_%=M z!18j8pFS%MW(rLSG1#pl9uzE8i+B+5y^B(@Z2one{sdqTZke@&lWPF~8{9c-TV4n+ z(^d7kDN_gG-!s?Ys}^5ZRaQi(t4iUD00r<f>*GR17}V-4fB2cThyz6_*D_+?z5!&^ zB@&ZrUN~%X?x^{T>IVMayE3`|yLM)H)$}ejzE-57&_j}zGq;N*EEfT9xH|EM!33|( zWAVy9?ValCodPium2Dedo$pIenlpQLWN2{k1dp?vN7zR5i|@JgIEt{oodOOGH<d>n z*TvakCG%O2IVyfW9y_%RE}glT`JIkc-fO~-9iS~7*2xe(_7ICyomaIhEce*6%Q!j_ zw<Ao0hobpIJFHLRRXh0|8m=j4u(br@>o#6ScK~DDBbnEeLqbHvEi5}fy&k9TRD1O^ z==Qul^;KK$>)nc$)>B*YpW5pkmd?pLE`H;kF>1ULYWT2;U-A6C-#qDO19zB)U8~Q+ zx4%909y`xPJAb={%O-E?AsR<lRi4<<M1LK)gWKjjEkL|Z4u9yy+j!T<g7Y~!BMP5O zRG#n5Qf_$ERvs%?Nw4+meUwS)yF2)`v(H+qPP<sl+P{Kdpmh;`dc$)sX8MB;3<YrG zd!{_lwsCN!B3@#*#PUk-N-R<Js<j&|qU%>>@Lys(?CgMD{=Ruv*wAZp<3m+j@!9fc zU4F_*Onpn|zpqn|zu96ECRp?DC5AUTDDWR`R!dV6dMvmEUl^aiTG2jKk(2ZIN|(^g z_a>n%y%*PW+u+J19P#L}jtfz|KJ@^%c7_ebdS+G7Gpwsn4$l<bAc}{a*;Z~8-Z1DG zt`%Md?WUxCZG-l^cAb~dSBP^kMf){<(!lByKeX$4=`*XoRC^jLbK4M1c-cHPB)Iw| zN~USGCv-NpYGWgR{Si3OnS1>6P=CK(jNt#QNT}k>4`}^yi(h+e{flbT{mj1qc5*mE zSS9b-)C&FgmrjF@ep8EF^Y*~Mg;0NXyzj{avNsXSU*_@mD)u)CT@>Ozh7VNX{?dpu zweAluy}m~>7J9&&_9|n>pRj*Rvd>XPB3_5?=+!C*`oFWAH{c*o-P&qJ`Cje%a;(V4 z%Qd$8yRV#Zj(CO74i66w-ri6?&tCJyCcZ2D8?sj6yN(HeUh&$dSGG_eM@U6g)6F<; zAFK1!(%$xYW^^bgB=f(AI%2%f@!v_>C00obLzlVp5p$7uXb3%&NQAfaSrLu+jjA^E z7aLvA`}XnLOznAh%hb<aHhUdq68FDfJmbR)n@=_LoL~Fby{L9trKrq57jfdW_Liv& z5g5;H<Ld0EYMO)h`&DQ6Qi%oam1`J2s$RIPzaFX&q*e`HRW^2*`fz?%A$KjN?6zC> z`y8=e>L(5=zLqF!dyVZCk-mysfb4;B{#zK%iTPn6`qPHa7x$V#=bzEu@^&|!ShWVZ z%ztaT{26f-^~+TY9ZVaUbyI5@fyZ(AeNbiTavV>gkiitud|5h1(h<L|9{zU#kAHgG z9m#70Q8&hjz}<^@G1u;OA8n_eZ)1$AO&uM!0o(5{nCV|S#6(11h`N41Di)I)#~;dT z<9C;?$8mhv*>WR29=ge#>$Y#S>8oxk^JvHet{5S)pLT2+myOE%v4zWzVxTrXS@jmC z_O_UfHUbM4+>qb!f{m;lN6y`>6J=zr)Sq#G(bF6-DhyGi(@}j9Pt>%lImsNSDxoyw z$fF$v`Y}=l+sy6hG2<)EP3Sn`4DAfdHBTM#Yy|)8*V<q33B$3mV)RVgu676>XwUV^ zdj$*9cL6^v_;5#Qw|o@{AxV79pK|nV4yIG*cK7GH5`o>Ph5qfK4t}%7<{Jz#uZ`xJ ze6oTwzpqS*DiokKwRo5#lbcuZ2eIuHcKr9K0nc>IltK3E_BG^X9@hsb2yw##C6rQs z6a$0UWHtJ-<M`o<Ow6H%nf}j8a&dFtlsqfX-2m9|4sP&UIz6Z&xiS0@Ol;>I?USf1 zN&Iele(sT}m2zypH;(}zERyH=079b^97X8v=Rwe%a2kLH3j<4@6znryqVm(tF0fX& zo<+NK{sZ*nF#N7xzs}P*v){W3I7$*8nRB7~#@PZ;AkyfQQBWMlQD%_t)W6>VWt!m^ z9v+V0h~Nj#Xi9&qw*^!Nc1DU>eilz=MoGC*-DLUHY1;|tlrb(*32G(dUr>VCG6vW7 zN{NZ7pKaS`#gsJ(`|FMEIKH@lEW!de#S9&#Q6kuI+Xhr(Uv<F~JBm!F=NKBA&Ujnk zzGLb+*7S#Eu5Io<7b|$0uB~3<0j~|CqoYn!Kgzq@?w<$Yl&egr&3X^~iq~)G+9A3_ zEy&tm*~O^XnBCbU#h~;O1Y2nhtnOO_L&NBizJt(<OiodyDFOdB2cD`Laz?|9`#HRY z=`!wsM+z`4Zj%!|<@a9XIX!N`v6MK+k>j#hbr1NU#a{}!m3kiXffWot03F*SPA`n* zcUrEl>d_ZFwZTO+IKkaGBZS&3tyi1udWm!~DgnPG2GO1wgiSC2zeFZXmvLiygM_j( z0MFr5QB}oyKWxkl!MfC)rHD0I7=0g#Eon_6`cORY8L*rf;M<vWSYuaMxZs9c=i70d zJMRHDAt8Cjb9cm-j`b4=DW`UtIA^!zH?)bSmz1x?P&3EHb5tF0fuJ^Yb8C;At!<J- z!X~8Q3<g1TIHFk`i0>kzXxf*b-2@ZXNXq2?G;SNecjRueQ7YSNey}Me0>1{(3!up~ zhBp#E{YzNI01Oz#0iMqIiDlW*Si0~E|E8fPgB~|14o|I#1+x1f?0Q=7iQQq3A3Y*u zmBAoL7@W)08913NC6r8dJ@|0alw0bG8n=yNxCn!a=b#0>R;Nv@f)IC87tp_DIAYwd zZUdlrg87HwC1Vm=rT{LhbUzI_!zh#)48+n-sKf10n!Rs7^DVF(05fU52wPuFeHOKb z(u59opZ1bd;iuR0qPYT}%wQ-VdLM?Alpaa1CJ$s`$+OcK)HBW_M-K&12_>zRUIg&} z9?Cy2cP-Syfk3=eANqzlg=`F5%8x+4B5B9P-{6!p7&NF8d1%u9MxHDDteb&Sz3M{$ z88HX0NMhBC#=)&U*lHq6XA@}}!;dO?fN1pR;hIMt0Ba4BH$sf|L%ySNJaPph;`qBY zhJ~&y&PsUi->yiO(+=(h=w9ZO@%UEiM<G&VOl%BBq6U<5F9+^n)LQh=5$4Y@G6_Y+ zOZ1f_eU4w3v5hqnueEf1akP_mBXn0sV_Dd?TB#rPYd&0Zd>DkBoix1}et^D7kow<a z*<tN;d8SKToP~F}RK_9Y#(WDV|DIHR4fE?8k7rGN+$JhefC%|Y?;0xleN8+ZC;Gn^ z{veqLh96+}f#c+&GABlwi|>T<?u6zLl6n1kRXpny<uNV}s>*lnE#2TT7uBTKP0Qet z!imI-u${rs&|3hRYutL%eQsN6j@Oi>G|FU7M|Vi_Vl02W3luJ>vyef9Qslem+3S`+ ze66t>v<GiYV(S?Js!!?MPFm*WPGI#SpFXp<6?F<aKxr$F0j`&WR&fW;FT)R4ihAqy z<;Xb?KAE|cn!p%-;6|fNrGDq5KoGYLgV^Vg&0=GC_N3l!ni-Fy8=nMgOjQ&Zf?VnA zusAmJs&8r{pQp;g$;t6!3~IWe)@YRQQYCOA9H4x~hlYl^3`vY?0md=hVxAHxaxD4v z6^KLB*{@=>0~qlfA8H0yLHZRQXk?HGG#QTuw2P8F0elF>@UWt~KzZi#d*&M$)EXD> zGi;nEfQYUt9|Olw>wlH>2h&a~0d|^4^RVucsKonLfFAm4=tt2g2dMgMeMWL_A<S#M zZbQUr(B#pz0&F!Y8a9X*N2RIh*G3vgHaY&=di7$`k3_kHrmLFfIY*?yq~u585Rzg% z)@q7s!vJ_+j`e)iDq&WUdatPz{#4QX&tWsP&oZcf2t@u<a*Rc!BmMS!@I6SW2~C7A z?mR*_GBqt@f9&^uuYo63w5?tMer1{+ss~gUq@gyc<mV5eGE<2+$#i+6jM{!m7g)TK zc}w1hNAa_kz<?>>8_}$nVn;L5HckHB4Lth5)Hhb*P)vOzIdOn1%XT@yq)5O#B&C{X z5osM{5W_<ztkDOX0bUZpA{?{=M@L+;ZcWsxk<rH-nwy5MK_-yT{Vn%z?#-7`@nFi< zT~V3?ylzkPxox5rC%VO{ngD#csR^N$+gm^w{TL8qgM=52OpyXK!U$Mz%CkY!7cM~A zK?8s;`RTp+NYilA&+%D!vH)SxfW$)jCmeR)Xjoye0|^A*4zaQmCkpmHLQ@fuK;lo1 z97r4Z)Sgw&&%oU75bEZalB$_nP<}}M0rGO(i_(W}1Nc@{%<{&5Qt1<zYNs?qc93rv zY&e_;w_pafGvHZhJ}khcE(O#DFlOjHCDxc@15|~Ba9Pxn79)0zPV0?MkOaNnz>6c; zuK&*X)B1W1AO~R5G<^ukD;lnSeX?Sh>Dx9k!~e<WVW{aShigu}AISXN)s?#Gy2q0I zqL%P6>GPV-iRYYti~lf{b!vTMVe#O%baii=(9+Y7?ui#&e`0k(Wcr~mb2B&n^*Ye( zQqj56w2Rq#d7p-!fOJeuMZs9ut;YRzeB491y=`tTZ{HWWROF0aDvo(%=~DMPuWjYG z-}e`C*S5JGe`n$2qVm_XrU<5r>XxLhFK<~C2DA4OXKAh1uq?{OojnXcG)#?sm>ge< z%CZd9OA)?*ghXeoikC8==JZ{qhzmhxhs4ena08)6a~{4<c=hT%*-Eg)Br{zB8f64V zRic{%o>N2Rl;d@%5F?xcX><z^T)0Hxpqlg0`Cf#Z^yJtFpCIV@yt7Y8w_AA`9a=P5 z7Nd(&x<rIo)44e{GI!mZEyJg7=B2mdsY~X*UFu241}4|8OO0->nB0)qI3jT-<@UI* z<nq4LV?F}L(czj4x;{Oedq#7OJ!U)3*U+<Cy?BmF13Rt}^;uAP5cCM)wQG&;>Gz!# zSLCJg%(3rg&Mo&@D2^+W{<dPzgsAk{uJb9u3xCagS@hKIlh^(aduD8%*svqP%)92z zWgQ90vkB)<+&LHO6SBfZ@S9@I2TfVNVMm3QV(yf?qA?-+j+>gg=VS~=R}^)w%#ZiT zPq8z}`xF>;V4c4F)6Q`Jy9Iu|wOv1YQ<mpGespcv$9jd~^Q=eTx9g<cIN_#~UR&!X z=rz<?pwYVRAj{L*?1RSB(oMZP#njWCVl>RXKA6fk3vJzUSH;cexN-WDx#1?seq*B- zK5!4H&h7y53NADeG?4wzZ?IS#?~3x~o7IW_)8UU8qEvaPc>D~EwJ-oJ)MGg&eX7?D zKbv=YET6MC@bT}i@v}bFU0J2WGT9F{d3BW}+GGwr&e@cvGT<@r<BOKm(@fUs%`%cR zj6MxC?C<^ZAa|+D<+~59oP6E#b;>)}PZ!+1g{Q0Wxc|oR8r##z7-@#8!*w5C+%FoN zB)unP*}n0Y3!Rwh?vUfbmF_?&3QN2iq7pFoCY={oY+BZN{?}NMdqw8Ly!n3<?j_0e z{oWh7Xl`^|h^nfpq<O(=5MiIA0sv4i0o9_EeHrnOW-UxaxJBh5fY{}*PXx9RtIX>D zyJX!*S5V(@yI8~K|FN;2$;sW-=6-yM>1bq(%eUS4ZmZ15KkqTw=3X)opED$0)j4$f zX>j$Bx`@jm<F~6{zu%uXbmv@cW<pcu-h&zE1u*8<ca_<tn$ZgnxO?5q3g>@Idpv%i zqb+vrnbSSh{Sc!T#gHu&&}CsZ&JckMmw@IC!?XSB*}Ws|o0hhADOG5p%>u#+jjlv_ z7Rd%mGJ%jELh&RJg9J$r2sQ)VjCm*OYijgtrptW+sZOT0zJ719bClBq?u-HDPv=tK z8D2tSsAi@fI_KE8z{hvhOPrP8JbK=I@^Za>+QUb(uj}ozM;_<ewYlg>7!<AiHc_Ff zF37p+vJR#DR*QsN_pgSa^a2=bNbue;RjH*UApT3p^2&IfxZuzg5t~ChjV)eBA1I!k zX4>L0+_S#m+QSWE(YeP$`=YwKT8rj<-fnJc*Y+*9*X^?B!alh|NAou;tz^De7ewEE zJu|;9!c(t@hj&H#`Nr5{@oj|%Wj=EEiZn_~TIklj&udG1ZeccRC-X!`@|N61OP8n` z#Y9El-)ivsBGMK>1di%>Z=5&7qfB(_Pvl3wb?#Kx2}a}r5Rt7f8vqmiBfgm6fT<s8 ooJ!eXWAs8QDV?K#v~wnB-Zn^BI>cV%#K1pV8oSi*sac)=KQGCHivR!s literal 0 HcmV?d00001 diff --git a/docs/experimentation.md b/docs/experimentation.md new file mode 100644 index 000000000..3745f67c2 --- /dev/null +++ b/docs/experimentation.md @@ -0,0 +1,27 @@ +# Experimentation + +## Introduction +In a nutshell, **Experimentation** module enables to run A/B/../n testing for your configurations in equi-sized groups/cohorts, it works on top of **Context-Aware-Config** and can also work as a release system for your configuration. + +## Concepts + +### Experiment +An experiment as name suggest enables you to test and evaluate the behaviour of the system for different values of the same configuration. An experiment can have exactly one **CONTROL** variant and `n` **EXPERIMENTAL** variants, with each variant overriding/changing same set of keys in configuration. An experiment's scope can be controlled by declaring the context, which chalks out the sample set for the experiment from the population. + +<br/> +<br/> + +![context-example](experiment-context-example.png) + +### Variant +A variant in an experiment, represent one of the `n` tests. In simple terms, its a collection of key-value pairs, where each key is your already defined **default-config** key, and the value can be any valid new/old configuration value for the key. There are two kinds of variants: +1. **CONTROL**: It conceptually represents the current state of the configuration. +2. **EXPERIMENTAL**: The experimental variant lets you define the newer value for the **default-config** keys. + +<br/> +<br/> + +![variant-example](experiment-variant-example.png) + +### Experiment's Traffic Percentage +This defines the traffic size for each variant of the experiment, for instance if traffic percentage is `13%` and there are `4` variants in the experiment, this makes each variant of the experiment receive `13%` of the entire traffic and in entirety `13 * 4 = 52%` of the total traffic. From 5a0f48aa6a0a25cfe24dc44b65cc350ea8e82749 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan <natarajan@juspay.in> Date: Thu, 8 Feb 2024 16:16:40 +0530 Subject: [PATCH 257/352] docs: add intro doc and features --- README.md | 55 ++++++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7af62f847..d69685d5f 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,22 @@ -# Context Aware Config # +# Superposition +Superposition is a cloud configuration and experimentation management platform that allows software teams to manage their configuration via a central location, run multi-variate experiments for different configuration values and evaluate results of these experiments and conclude them accordingly. +The Superposition platform comes with three components: -### How do I get set up? ### +* **Context-Aware-Configuration** - a flexible configuration management system that supports contextual overrides for configuration keys +* **Experimentation** - a experimentation management system that allows supplying different configuration values to equal-sized cohorts (facilitating A/B testing) +* **Metrics** - a metrics sub-system that interacts with analytics backends to provide supporting metrics that enable conclusions to be drawn from experiments (TBD) -* Clone this repo -* [Install nix](https://zero-to-nix.com/start/install) -* Start docker daemon either through docker desktop or using the command given below - ```bash - $ open --background -a Docker - ``` -* Make sure your brew's postgres service is not running +## Detailed documentation +1. [Context-Aware-Configuration](docs/context-aware-configuration.md) +2. [Experimentation](docs/context-aware-configuration.md) +3. [Metrics](docs/metrics.md) +4. [Clients](docs/clients.md) +5. [Local setup](docs/setup.md) - ```bash - # Check if service is running - $ brew services - # If postgres is running, stop it via brew - $ brew services stop postgresql@<your_postgres_version> - $ cd context-aware-config - ``` -* cd to the repo dir - ```bash - $ cd context-aware-config - ``` -* run these commands - ```bash - $ nix develop - $ make setup - $ make run OR - $ make run -e DOCKER_DNS=localhost - ``` -* If you get something like this in the output, you are good to go - ```bash - {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.291Z","value":"starting 5 workers"} - {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.292Z","value":"Actix runtime found; starting in Actix runtime"} - ``` -* You can hit the `/health` endpoint to confirm server is ready to serve - ```bash - $ curl --location 'http://localhost:8080/health' - #Expected Response : "Health is good :D" - ``` +## Key features +* **Admin UI** - Out of the box administration (and tools) UI for configurations and experiments +* **Rich API support** - every action on the platform to manage configurations / experiments is supported with an accompanying API +* **Type/Validation support** - Comprehensive type support using json-schema and custom validator function support for configuration values +* **Multi-tenant support** - a single deployment allows multiple tenants to manage their configurations/experiments in a completely isolated manner +* **Authn/Authz support** - control who can make configuration/experimentation changes \ No newline at end of file From eb1fba392d386e79ee8569ea28a486f0938bd69a Mon Sep 17 00:00:00 2001 From: Natarajan Kannan <natarajan@juspay.in> Date: Thu, 8 Feb 2024 18:01:43 +0530 Subject: [PATCH 258/352] docs: add intro doc and features --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d69685d5f..e5497d0f6 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,16 @@ The Superposition platform comes with three components: ## Detailed documentation 1. [Context-Aware-Configuration](docs/context-aware-configuration.md) -2. [Experimentation](docs/context-aware-configuration.md) +2. [Experimentation](docs/experimentation.md) 3. [Metrics](docs/metrics.md) -4. [Clients](docs/clients.md) +4. [Client Context-Aware-Configuration](docs/client-context-aware-configuration.md) +4. [Client Experimentation](docs/client-experimentation.md) 5. [Local setup](docs/setup.md) -## Key features +## Key highlights * **Admin UI** - Out of the box administration (and tools) UI for configurations and experiments * **Rich API support** - every action on the platform to manage configurations / experiments is supported with an accompanying API +* **Safe configuration changes** - support canary testing for releasing configuration changes using experiments * **Type/Validation support** - Comprehensive type support using json-schema and custom validator function support for configuration values * **Multi-tenant support** - a single deployment allows multiple tenants to manage their configurations/experiments in a completely isolated manner * **Authn/Authz support** - control who can make configuration/experimentation changes \ No newline at end of file From 28e06946df1480ebc071a78de27b3682b71726d4 Mon Sep 17 00:00:00 2001 From: Natarajan Kannan <natarajan@juspay.in> Date: Thu, 8 Feb 2024 19:25:04 +0530 Subject: [PATCH 259/352] docs: add intro doc and features --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5497d0f6..830b2af34 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The Superposition platform comes with three components: * **Metrics** - a metrics sub-system that interacts with analytics backends to provide supporting metrics that enable conclusions to be drawn from experiments (TBD) ## Detailed documentation -1. [Context-Aware-Configuration](docs/context-aware-configuration.md) +1. [Context-Aware-Configuration](docs/context-aware-config.md) 2. [Experimentation](docs/experimentation.md) 3. [Metrics](docs/metrics.md) 4. [Client Context-Aware-Configuration](docs/client-context-aware-configuration.md) From 7f21af50995b0ea17bcb8f98c993432189073cb6 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Mon, 12 Feb 2024 10:55:35 +0530 Subject: [PATCH 260/352] chore: autodeploy to sbx --- Jenkinsfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index dc7dd59a3..415708282 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ pipeline { REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); REGISTRY_HOST_NY_PROD = getRegistryHost("147728078333", REGION); - AUTOPILOT_HOST_INTEG = "autopilot-eks2.internal.svc.k8s.integ.mum.juspay.net"; + AUTOPILOT_HOST_SBX = "autopilot.internal.staging.mum.juspay.net"; DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" GIT_REPO_NAME = "context-aware-config" CARGO_NET_GIT_FETCH_WITH_CLI=true @@ -206,29 +206,31 @@ pipeline { AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") } steps { - sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_INTEG}/release' \ + sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_SBX}/release' \ --header 'Content-Type: application/json' \ - --header 'Authorization: Basic ${CREDS_PSW}' \ + --header 'Authorization: Basic ${AUTOPILOT_AUTH_HEADER}' \ --data-raw '{ "service": ["CONTEXT_AWARE_CONFIG"], - "release_manager": "${AUTHOR_NAME}", + "release_manager": "jenkins.jenkins", "release_tag": "", "new_version": "${NEW_SEMANTIC_VERSION}", "docker_image" : "${NEW_SEMANTIC_VERSION}", "priority" : 0, - "cluster" : "INTEG_CLUSTER", + "cluster" : "EKS_MUM", + "is_approved": 1, + "is_infra_approved": 1, "change_log": "${CHANGE_LOG}", "rollout_strategy": [ { "rollout": 100, - "cooloff": 0, + "cooloff": 1, "pods": 1 } ], "description": "${CHANGE_LOG}", "product": "HYPER_SDK", "mode" : "AUTO", - "env" : "INTEG" + "env" : "UAT" }'; """ } From ee2254fd6c5f3bfe853d78de411909b77c3cc9df Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Thu, 15 Feb 2024 10:39:16 +0530 Subject: [PATCH 261/352] fix: jenkinsfile now sends build alerts in channel --- Jenkinsfile | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 415708282..d8dae660e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,13 @@ +def jenkinsConsoleUrl = "$env.JOB_URL" + "$env.BUILD_NUMBER" +def slackResponseArray = [] + +def sendSlack(color, message, jenkinsConsoleUrl, slackResponseArray) { + def slackResponse = slackSend channel: "C04QY9BFM7Y", color: color, message: message+ "\n" + "<${jenkinsConsoleUrl} | *JOB-${env.BUILD_NUMBER}*>" + slackResponseArray.each { item -> + slackSend(channel: slackResponse.threadId, color: "#01F2D1" , message: "`${item}`" ) + } +} + def getRegistryHost(aws_acc_id, region) { return aws_acc_id + ".dkr.ecr." + region + ".amazonaws.com"; } @@ -140,6 +150,10 @@ pipeline { branch 'main' } steps { + script { + slackResponseArray << "COMMIT BUILT : ${COMMIT_HASH}" + slackResponseArray << "NEW_SEMANTIC_VERSION/DOCKER IMAGE TAG : ${NEW_SEMANTIC_VERSION}" + } sh '''make ci-build -e \ VERSION=${NEW_SEMANTIC_VERSION} \ SOURCE_COMMIT=${COMMIT_HASH} \ @@ -244,4 +258,20 @@ pipeline { } } } + post { + failure { + script { + if (env.BRANCH_NAME == 'main') { + sendSlack("#AA1100", "@channel *BUILD_FAILED* ", jenkinsConsoleUrl, slackResponseArray) + } + } + } + success { + script { + if (env.BRANCH_NAME == 'main' && env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION) { + sendSlack("#3CF700", "*BUILD SUCCESS*", jenkinsConsoleUrl, slackResponseArray) + } + } + } + } } From 3d64762cdbce4a5526aaeb947f8f9a7ce864dbb4 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 22 Jan 2024 13:59:58 +0000 Subject: [PATCH 262/352] fix: fixing error message for experiment create and bulk context api --- Cargo.lock | 1 + .../src/api/context/handlers.rs | 31 +++- .../src/api/context/types.rs | 12 ++ .../src/api/experiments/handlers.rs | 143 ++++++++++++------ .../src/components/context_form/utils.rs | 56 +++++-- .../default_config_form.rs | 50 +++--- .../experiment_form/experiment_form.rs | 14 +- .../src/components/experiment_form/utils.rs | 5 +- .../components/override_form/override_form.rs | 44 +++++- .../pages/ContextOverride/ContextOverride.rs | 110 +++----------- crates/service-utils/Cargo.toml | 1 + crates/service-utils/src/helpers.rs | 16 +- 12 files changed, 296 insertions(+), 187 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 806feb18d..3723d303e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3405,6 +3405,7 @@ dependencies = [ "futures-util", "jsonschema", "log", + "reqwest", "rs-snowflake", "rusoto_core", "rusoto_kms", diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 3cd6f0a8c..f12a4a2e2 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -3,7 +3,7 @@ use crate::{ api::{ context::types::{ ContextAction, ContextBulkResponse, DimensionCondition, MoveReq, - PaginationParams, PutReq, PutResp, + PaginationParams, PutReq, PutResp, TransactionError, }, dimension::get_all_dimension_schema_map, }, @@ -489,7 +489,7 @@ async fn bulk_operations( let DbConnection(mut conn) = db_conn; let mut resp = Vec::<ContextBulkResponse>::new(); - let result = conn.transaction::<_, diesel::result::Error, _>(|transaction_conn| { + let result = conn.transaction::<_, TransactionError, _>(|transaction_conn| { for action in reqs.into_inner().into_iter() { match action { ContextAction::PUT(put_req) => { @@ -502,7 +502,13 @@ async fn bulk_operations( } Err(e) => { log::error!("Failed at insert into contexts due to {:?}", e); - return Err(diesel::result::Error::RollbackTransaction); + if e.to_string().contains("Bad schema") { + return Err(TransactionError::BadRequest(e.to_string())); + } else { + return Err(TransactionError::DieselError( + diesel::result::Error::RollbackTransaction, + )); + } } } } @@ -511,7 +517,12 @@ async fn bulk_operations( delete(contexts.filter(id.eq(&ctx_id))).execute(transaction_conn); let email = user.clone().email; match deleted_row { - Ok(0) => return Err(diesel::result::Error::RollbackTransaction), + // Any kind of error would rollback the tranction but explicitly returning rollback tranction allows you to rollback from any point in transaction. + Ok(0) => { + return Err(TransactionError::DieselError( + diesel::result::Error::RollbackTransaction, + )) + } Ok(_) => { log::info!("{ctx_id} context deleted by {email}"); resp.push(ContextBulkResponse::DELETE(format!( @@ -520,7 +531,9 @@ async fn bulk_operations( } Err(e) => { log::error!("Delete context failed due to {:?}", e); - return Err(diesel::result::Error::RollbackTransaction); + return Err(TransactionError::DieselError( + diesel::result::Error::RollbackTransaction, + )); } }; } @@ -540,7 +553,9 @@ async fn bulk_operations( "Failed at moving context reponse due to {:?}", e ); - return Err(diesel::result::Error::RollbackTransaction); + return Err(TransactionError::DieselError( + diesel::result::Error::RollbackTransaction, + )); } }; } @@ -548,9 +563,9 @@ async fn bulk_operations( } Ok(()) // Commit the transaction }); - match result { Ok(_) => Ok(web::Json(resp)), - Err(_) => Err(ErrorInternalServerError("")), // If the transaction failed, return an error + Err(TransactionError::BadRequest(_)) => Err(ErrorBadRequest("")), + Err(TransactionError::DieselError(_)) => Err(ErrorInternalServerError("")), } } diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index d29760c40..d9097b02e 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -43,3 +43,15 @@ pub enum ContextBulkResponse { DELETE(String), MOVE(PutResp), } + +#[derive(Debug)] +pub enum TransactionError { + DieselError(diesel::result::Error), + BadRequest(String), // Custom error type for bad requests +} + +impl From<diesel::result::Error> for TransactionError { + fn from(error: diesel::result::Error) -> Self { + TransactionError::DieselError(error) + } +} diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index f48673785..6621236e2 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -12,8 +12,12 @@ use diesel::{ ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; +use service_utils::errors::types::{Error, ServerError}; + +use reqwest::{Response, StatusCode}; use service_utils::{ errors::types::{Error as err, ErrorResponse}, + helpers::remove_error_abstraction, service::types::{AppState, DbConnection, Tenant}, types as app, }; @@ -50,6 +54,60 @@ pub fn endpoints(scope: Scope) -> Scope { .service(update_overrides) } +async fn process_http_response( + response: Result<Response, reqwest::Error>, +) -> Result<Option<Vec<ContextPutResp>>, Error> { + match response { + Ok(res) if res.status().is_success() => { + match res.json::<Vec<ContextBulkResponse>>().await { + Ok(bulk_responses) => { + let contexts = + bulk_responses + .into_iter() + .fold(Vec::new(), |mut acc, item| { + if let ContextBulkResponse::PUT(context) = item { + acc.push(context); + } else { + log::error!("Unexpected response item: {:?}", item); + } + acc + }); + Ok(Some(contexts)) + } + Err(e) => { + log::error!("Failed to parse JSON response: {}", e); + Err(Error::InternalServerErr(e.to_string())) + } + } + } + Ok(res) => { + let error_response = ErrorResponse { + message: format!("HTTP error with status: {}", res.status().as_u16()), + possible_fix: String::from("Please check the request and try again."), + }; + match res.status() { + StatusCode::BAD_REQUEST => Err(Error::BadRequest(error_response)), + StatusCode::NOT_FOUND => Err(Error::NotFound(error_response)), + StatusCode::INTERNAL_SERVER_ERROR => Err(Error::InternalServerErr( + "Internal server error occurred".to_string(), + )), + _ => Err(Error::Generic(ServerError { + message: "Unexpected HTTP response".into(), + possible_fix: "Please contact support".into(), + status_code: res.status(), + })), + } + } + Err(e) => { + log::error!("HTTP request failed: {}", e); + Err(Error::ConnectionFailed( + "External API".into(), + e.to_string(), + )) + } + } +} + #[post("")] async fn create( state: Data<AppState>, @@ -152,7 +210,8 @@ async fn create( let http_client = reqwest::Client::new(); let url = state.cac_host.clone() + "/context/bulk-operations"; - let created_contexts: Vec<ContextPutResp> = http_client + // Step 1: Perform the HTTP request and handle errors + let response = http_client .put(&url) .header( "Authorization", @@ -161,33 +220,21 @@ async fn create( .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() - .await - .map_err(|e| err::InternalServerErr(e.to_string()))? - .json::<Vec<ContextBulkResponse>>() - .await - .map_err(|e| err::InternalServerErr(e.to_string()))? - .into_iter() - .fold( - Vec::<ContextPutResp>::new(), - |mut put_responses, response| { - if let ContextBulkResponse::PUT(created_context) = response { - put_responses.push(created_context); - } else { - log::error!( - "unexpected response from cac for create only request: {:?}", - response - ); - } - put_responses - }, - ); + .await; - // updating variants with context and override ids - for i in 0..created_contexts.len() { - let created_context = &created_contexts[i]; + let created_contexts = process_http_response(response).await?; - variants[i].context_id = Some(created_context.context_id.clone()); - variants[i].override_id = Some(created_context.override_id.clone()); + match created_contexts { + Some(contexts) => { + for i in 0..contexts.len() { + let created_context = &contexts[i]; + variants[i].context_id = Some(created_context.context_id.clone()); + variants[i].override_id = Some(created_context.override_id.clone()); + } + } + None => { + log::info!("No contexts were created or returned."); + } } // inserting experiment in db @@ -316,9 +363,9 @@ pub async fn conclude( .json(&operations) .send() .await - .map_err(|e| err::InternalServerErr(e.to_string()))?; + .map_err(|e| remove_error_abstraction(e))?; - if !response.status().is_success() { + if response.status().is_server_error() { return Err(err::InternalServerErr(format!( "Request to {} failed with response: {:?}", url, response @@ -641,7 +688,7 @@ async fn update_overrides( let http_client = reqwest::Client::new(); let url = state.cac_host.clone() + "/context/bulk-operations"; - let created_contexts: Vec<ContextPutResp> = http_client + let response = http_client .put(&url) .header( "Authorization", @@ -650,31 +697,29 @@ async fn update_overrides( .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() - .await - .map_err(|e| err::InternalServerErr(e.to_string()))? - .json::<Vec<ContextBulkResponse>>() - .await - .map_err(|e| err::InternalServerErr(e.to_string()))? - .into_iter() - .fold( - Vec::<ContextPutResp>::new(), - |mut put_responses, response| { - if let ContextBulkResponse::PUT(created_context) = response { - put_responses.push(created_context); - } - put_responses - }, - ); + .await; - /*************************** Updating experiment in DB **************************/ + let created_contexts = process_http_response(response).await; - for i in 0..created_contexts.len() { - let created_context = &created_contexts[i]; + match created_contexts { + Ok(Some(contexts)) => { + for i in 0..contexts.len() { + let created_context = &contexts[i]; - new_variants[i].context_id = Some(created_context.context_id.clone()); - new_variants[i].override_id = Some(created_context.override_id.clone()); + new_variants[i].context_id = Some(created_context.context_id.clone()); + new_variants[i].override_id = Some(created_context.override_id.clone()); + } + } + Ok(None) => { + log::info!("No contexts were created or returned."); + } + Err(e) => { + log::error!("An error occurred: {}", e); + } } + /*************************** Updating experiment in DB **************************/ + let new_variants_json = serde_json::to_value(new_variants).map_err(|e| { err::InternalServerErr(format!("failed to convert new variants to json: {e}")) })?; diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index f8b300374..694a1a52e 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,40 +1,76 @@ +use crate::types::Dimension; use crate::utils::get_host; use reqwest::StatusCode; use serde_json::{json, Map, Value}; +use std::str::FromStr; -pub fn get_condition_schema(var: &str, op: &str, val: &str) -> Value { +pub fn get_dimension_type(dimensions: Vec<Dimension>, dimension_name: &str) -> String { + let dimension = dimensions + .iter() + .find(|&dimension| dimension.dimension == dimension_name.to_string()); + let schema = &dimension.unwrap().schema; + let schema_type = schema.get("type").unwrap(); + schema_type.to_string() +} + +pub fn get_condition_schema( + var: &str, + op: &str, + val: &str, + dimensions: Vec<Dimension>, +) -> Value { + let dimension_value = |variable: &str, val: &str, dimensions: &Vec<Dimension>| { + let dimension_type = get_dimension_type(dimensions.clone(), variable); + match dimension_type.replace("\"", "").as_str() { + "boolean" => match bool::from_str(val) { + Ok(boolean) => Value::Bool(boolean), + _ => Value::String("Invalid Boolean".to_string()), + }, + "number" => match val.parse::<i64>() { + Ok(number) => Value::Number(number.into()), + Err(_) => Value::String(val.to_string()), + }, + _ => Value::String(val.to_string()), + } + }; match op { "<=" => { let mut split_value = val.split(','); + let first_operand = split_value.next().unwrap().trim().parse::<i64>().unwrap(); - let second_operand = - split_value.next().unwrap().trim().parse::<i64>().unwrap(); + + let dimension_val = + dimension_value(var, split_value.next().unwrap().trim(), &dimensions); json!({ op: [ first_operand, { "var": var }, - second_operand + dimension_val ] }) } _ => { + let dimension_val = dimension_value(var, val, &dimensions); json!({ op: [ {"var": var}, - val + dimension_val ] }) } } } -pub fn construct_context(conditions: Vec<(String, String, String)>) -> Value { +pub fn construct_context( + conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, +) -> Value { let condition_schemas = conditions .iter() .map(|(variable, operator, value)| { - get_condition_schema(variable, operator, value) + get_condition_schema(variable, operator, value, dimensions.clone()) }) .collect::<Vec<Value>>(); @@ -50,12 +86,13 @@ pub fn construct_context(conditions: Vec<(String, String, String)>) -> Value { pub fn construct_request_payload( overrides: Map<String, Value>, conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, ) -> Value { // Construct the override section let override_section: Map<String, Value> = overrides; // Construct the context section - let context_section = construct_context(conditions); + let context_section = construct_context(conditions, dimensions); // Construct the entire request payload let request_payload = json!({ @@ -70,11 +107,12 @@ pub async fn create_context( tenant: String, overrides: Map<String, Value>, conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, ) -> Result<String, String> { let client = reqwest::Client::new(); let host = get_host(); let url = format!("{host}/context"); - let request_payload = construct_request_payload(overrides, conditions); + let request_payload = construct_request_payload(overrides, conditions, dimensions); let response = client .put(url) .header("x-tenant", tenant) diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index b79ed8fc7..4669f9b6d 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -207,7 +207,9 @@ where {move || { view! { - <Show when=move || ((config_type.get() == "number") || (config_type.get() == "decimal"))> + <Show when=move || { + (config_type.get() == "number") || (config_type.get() == "decimal") + }> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Value</span> @@ -226,7 +228,10 @@ where </div> </Show> - <Show when=move || (show_labels.get() && (config_type.get() != "number") && (config_type.get() != "decimal") )> + <Show when=move || { + show_labels.get() && (config_type.get() != "number") + && (config_type.get() != "decimal") + }> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Value</span> @@ -241,29 +246,30 @@ where set_config_value.set(event_target_value(&ev)); } /> + </div> - <Show when= move || (config_type.get() != "boolean")> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> - {config_type.get()} - </span> - </label> - <textarea - type="text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - on:change=move |ev| { - let value = event_target_value(&ev); - logging::log!("{:?}", value); - set_config_pattern.set(value); - } - > + <Show when=move || (config_type.get() != "boolean")> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {config_type.get()} + </span> + </label> + <textarea + type="text" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_config_pattern.set(value); + } + > - {config_pattern.get()} - </textarea> + {config_pattern.get()} + </textarea> - </div> - </Show> + </div> + </Show> </Show> } }} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 3584871f4..004132cc4 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -76,6 +76,7 @@ where handle_change }; + let dimension_on_submit = dimensions.clone(); let on_submit = move |event: MouseEvent| { event.prevent_default(); logging::log!("Submitting experiment form"); @@ -91,6 +92,7 @@ where let tenant = tenant_rs.get(); let experiment_id = id.clone(); let handle_submit_clone = handle_submit.clone(); + let dimensions = dimension_on_submit.clone(); logging::log!("{:?}", f_experiment_name); logging::log!("{:?}", f_context); @@ -100,8 +102,14 @@ where let result = if edit { update_experiment(experiment_id, f_variants, tenant).await } else { - create_experiment(f_context, f_variants, f_experiment_name, tenant) - .await + create_experiment( + f_context, + f_variants, + f_experiment_name, + tenant, + dimensions.clone(), + ) + .await }; match result { @@ -137,7 +145,7 @@ where <div class="my-4"> <ContextForm - dimensions=dimensions + dimensions={dimensions.clone()} context=context handle_change=handle_context_form_change is_standalone=false diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index dfcde03ed..81a4f3668 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -2,7 +2,7 @@ use super::types::{ ExperimentCreateRequest, ExperimentUpdateRequest, VariantUpdateRequest, }; use crate::components::context_form::utils::construct_context; -use crate::types::Variant; +use crate::types::{Dimension, Variant}; use crate::utils::get_host; use reqwest::StatusCode; use serde_json::json; @@ -12,11 +12,12 @@ pub async fn create_experiment( variants: Vec<Variant>, name: String, tenant: String, + dimensions: Vec<Dimension>, ) -> Result<String, String> { let payload = ExperimentCreateRequest { name, variants, - context: construct_context(conditions), + context: construct_context(conditions, dimensions), }; let client = reqwest::Client::new(); diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 4fc9ccde6..0e629f3d9 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -2,8 +2,22 @@ use crate::types::DefaultConfig; use leptos::*; use serde_json::{json, Map, Value}; use std::collections::HashSet; +use std::rc::Rc; +use std::str::FromStr; use web_sys::MouseEvent; +pub fn get_default_config_type( + default_configs: Vec<DefaultConfig>, + key_name: &str, +) -> String { + let default_config = default_configs + .iter() + .find(|&default_conf| default_conf.key == key_name.to_string()); + let schema = &default_config.unwrap().schema; + let schema_type = schema.get("type").unwrap(); + schema_type.to_string() +} + #[component] pub fn override_form<NF>( overrides: Map<String, Value>, @@ -28,6 +42,24 @@ where logging::log!("{:?}", overrides.get()); }; + let default_config_rc = Rc::new(default_config.clone()); + + let default_config_value = + |name: &str, val: &str, default_configs: &Vec<DefaultConfig>| { + let dimension_type = get_default_config_type(default_configs.clone(), name); + match dimension_type.replace("\"", "").as_str() { + "boolean" => match bool::from_str(val) { + Ok(boolean) => Value::Bool(boolean), + _ => Value::String("Invalid Boolean".to_string()), + }, + "number" => match val.parse::<i64>() { + Ok(number) => Value::Number(number.into()), + Err(_) => Value::String(val.to_string()), + }, + _ => Value::String(val.to_string()), + } + }; + create_effect(move |_| { let f_override = overrides.get(); handle_change(f_override.clone()); @@ -105,6 +137,7 @@ where let config_key_label = config_key.to_string(); let config_key_value = config_key.to_string(); let config_value = config_value.to_string().replace("\"", ""); + let default_config_clone = default_config_rc.clone(); view! { <div> <div class="flex items-center gap-4"> @@ -122,10 +155,19 @@ where value=config_value on:input=move |event| { let input_value = event_target_value(&event); + let default_config_val = default_config_value( + &config_key_value, + &input_value, + &default_config_clone.clone(), + ); + logging::log!("Default Config: {}", default_config_val); set_overrides .update(|curr_overrides| { curr_overrides - .insert(config_key_value.to_string(), json!(input_value)); + .insert( + config_key_value.to_string(), + json!(default_config_val), + ); }); } /> diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index d8f3bf0a6..86e7e4c56 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -5,26 +5,23 @@ use crate::api::{fetch_default_config, fetch_dimensions}; use crate::components::button::button::Button; use crate::components::condition_pills::condition_pills::ContextPills; use crate::components::context_form::context_form::ContextForm; +use crate::components::context_form::utils::create_context; use crate::components::override_form::override_form::OverrideForm; use crate::components::table::{table::Table, types::Column}; -use crate::utils::{get_host, modal_action}; +use crate::types::Dimension; +use crate::utils::modal_action; use leptos::*; -use reqwest::StatusCode; -use serde_json::{json, Map, Value}; +use serde_json::{Map, Value}; use web_sys::MouseEvent; #[component] -fn ContextModalForm<NF>(handle_change: NF) -> impl IntoView +fn ContextModalForm<NF>( + handle_change: NF, + dimensions: Resource<String, Result<Vec<Dimension>, ServerFnError>>, +) -> impl IntoView where NF: Fn(Vec<(String, String, String)>) + 'static + Clone, { - let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - - let dimensions = create_blocking_resource( - move || tenant_rs.get(), - move |current_tenant| fetch_dimensions(current_tenant.clone()), - ); - view! { <div> <Suspense fallback=move || { @@ -70,86 +67,6 @@ where } } -pub fn construct_request_payload( - overrides: Map<String, Value>, - conditions: Vec<(String, String, String)>, -) -> Value { - // Construct the override section - let override_section: Map<String, Value> = overrides; - let between_operators = |val: &str, variable: &str| { - let split_value: Vec<&str> = val.split(',').collect(); - json!({ - "<=": [ - split_value[0].trim(), - { "var": variable }, - split_value[1].trim() - ] - }) - }; - - let other_operators = |op: &str, val: &str, variable: &str| { - json!({ - op: [ - { "var": variable }, - val - ] - }) - }; - - let context_section = if conditions.len() == 1 { - let (variable, operator, value) = &conditions[0]; - if operator == "<=" { - between_operators(value, variable) - } else { - other_operators(operator, value, variable) - } - } else { - let and_conditions: Vec<Value> = conditions - .iter() // Use iter() instead of into_iter() to avoid consuming conditions - .map(|(variable, operator, value)| { - if operator == "<=" { - between_operators(value, variable) - } else { - other_operators(operator, value, variable) - } - }) - .collect(); - - json!({ "and": and_conditions }) - }; - - // Construct the entire request payload - let request_payload = json!({ - "override": override_section, - "context": context_section - }); - - request_payload -} - -pub async fn create_context( - tenant: String, - overrides: Map<String, Value>, - conditions: Vec<(String, String, String)>, -) -> Result<String, String> { - let client = reqwest::Client::new(); - let host = get_host(); - let url = format!("{host}/context"); - let request_payload = construct_request_payload(overrides, conditions); - let response = client - .put(url) - .header("x-tenant", tenant) - .json(&request_payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), - _ => Err("Internal Server Error".to_string()), - } -} - #[component] fn OverrideModalForm<NF>(handle_change: NF) -> impl IntoView where @@ -221,6 +138,11 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { set_overrides.set(updated_overrides); }; + let dimensions = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_dimensions(current_tenant.clone()), + ); + let (error_message, set_error_message) = create_signal("".to_string()); let on_submit = { @@ -238,6 +160,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { current_tenant, overrides.get(), context_condition.get(), + dimensions.get().unwrap().expect("resource not loaded"), ) .await; @@ -270,7 +193,10 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { </form> <form class="form-control w-full mt-8 bg-white text-gray-700 font-mono"> <div> - <ContextModalForm handle_change=handle_context_change/> + <ContextModalForm + handle_change=handle_context_change + dimensions=dimensions + /> </div> <div class="mt-7"> <OverrideModalForm handle_change=handle_overrides_change/> diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index f46fb7201..42ef59054 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -32,3 +32,4 @@ serde = { workspace = true } serde_json = { workspace = true } derive_more = { workspace = true } dashboard-auth = { workspace = true } +reqwest = { workspace = true } \ No newline at end of file diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index e808e3923..04fc1af53 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -1,4 +1,4 @@ -use actix_web::{error::ErrorInternalServerError, Error}; +use actix_web::{error::ErrorInternalServerError, http::StatusCode, Error}; use log::info; use serde::de::{self, IntoDeserializer}; use std::{ @@ -7,6 +7,8 @@ use std::{ str::FromStr, }; +use super::errors::types::{Error as err, ErrorResponse}; + //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should //only be used for envs needed during app start. @@ -119,3 +121,15 @@ pub fn get_pod_info() -> (String, String) { ); (pod_id, deployment_id) } + +pub fn remove_error_abstraction(e: reqwest::Error) -> err { + match e.status() { + Some(StatusCode::BAD_REQUEST) => err::BadRequest(ErrorResponse { + message: e.to_string(), + possible_fix: + "Please try again with correct value. Schema type / Validation is failing" + .to_string(), + }), + _ => err::InternalServerErr(e.to_string()), + } +} From ce58f474fca05f339dde6fc07f68aeba2312dd48 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 15 Feb 2024 18:32:41 +0530 Subject: [PATCH 263/352] ci: pushing cac image to NY sbx ECR --- Jenkinsfile | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d8dae660e..ac5fc2463 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,6 +22,7 @@ pipeline { REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); REGISTRY_HOST_NY_PROD = getRegistryHost("147728078333", REGION); + REGISTRY_HOST_NY_SBX = getRegistryHost("463356420488", REGION); AUTOPILOT_HOST_SBX = "autopilot.internal.staging.mum.juspay.net"; DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" GIT_REPO_NAME = "context-aware-config" @@ -192,6 +193,21 @@ pipeline { } } + stage('Push Image To NY Sandbox Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_NY_SBX} + ''' + } + } + stage('Push Image To NY Production Registry') { when { expression { SKIP_CI == 'false' } @@ -214,7 +230,7 @@ pipeline { branch 'main' } environment { - CREDS = credentials('AP_INTEG_ID') + CREDS = credentials('AUTOPILOT_AUTH_HEADER') COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") @@ -222,7 +238,7 @@ pipeline { steps { sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_SBX}/release' \ --header 'Content-Type: application/json' \ - --header 'Authorization: Basic ${AUTOPILOT_AUTH_HEADER}' \ + --header 'Authorization: Basic ${CREDS}' \ --data-raw '{ "service": ["CONTEXT_AWARE_CONFIG"], "release_manager": "jenkins.jenkins", @@ -274,4 +290,4 @@ pipeline { } } } -} +} \ No newline at end of file From 975ecf7526b4fc235d8a05cb05204954e30529c3 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 15 Feb 2024 16:33:29 +0000 Subject: [PATCH 264/352] chore(version): v0.20.0 [skip ci] --- CHANGELOG.md | 26 ++++++++++++++++++++ Cargo.lock | 6 ++--- crates/context-aware-config/CHANGELOG.md | 6 +++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 +++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 +++++ crates/service-utils/Cargo.toml | 4 +-- 8 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e11dbe8c..464da5f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.20.0 - 2024-02-15 +### Package updates +- service-utils bumped to service-utils-v0.10.3 +- experimentation-platform bumped to experimentation-platform-v0.9.3 +- context-aware-config bumped to context-aware-config-v0.15.1 +### Global changes +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins +- jenkinsfile now sends build alerts in channel - (2e04ca5) - Kartik +#### Continuous Integration +- pushing cac image to NY sbx ECR - (78c1b32) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (4dc6f19) - Natarajan Kannan +- PICAF-25981: add intro doc and features - (14a7f44) - Natarajan Kannan +- PICAF-25981: add intro doc and features - (0e11056) - Natarajan Kannan +- [PICAF-25981] context aware config docs - (ea04b76) - Kartik +- (PICAF-25983) added setup instruction - (e7d00d9) - Saurav Suman +#### Features +- [PICAF-25981] client-integration-doc - (bc4927d) - Pratik Mishra +- added bool, i64 and decimal in default config form - (fca1ca6) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-25973] autodeploy to sbx - (b812140) - Kartik +- experimentation docs first cut - (d81aea4) - Shubhranshu Sanjeev + +- - - + ## v0.19.0 - 2024-01-31 ### Package updates - context-aware-config bumped to context-aware-config-v0.15.0 diff --git a/Cargo.lock b/Cargo.lock index 3723d303e..263c7e017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,7 +854,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.15.0" +version = "0.15.1" dependencies = [ "actix", "actix-cors", @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.9.2" +version = "0.9.3" dependencies = [ "actix", "actix-web", @@ -3390,7 +3390,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.10.2" +version = "0.10.3" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 05fa18faf..08a394897 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.15.1 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + ## context-aware-config-v0.15.0 - 2024-01-31 #### Features - [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 70a71b614..6c08f48a1 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.15.0" +version = "0.15.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 2be4a21df..85c6d8625 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.9.3 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + ## experimentation-platform-v0.9.2 - 2024-01-29 #### Bug Fixes - added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 1b3fef7f1..2b6bd8ec3 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.9.2" +version = "0.9.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 364739e4b..4cc650202 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.10.3 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + ## service-utils-v0.10.2 - 2024-01-22 #### Bug Fixes - fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 42ef59054..252ca1c0c 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.10.2" +version = "0.10.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -32,4 +32,4 @@ serde = { workspace = true } serde_json = { workspace = true } derive_more = { workspace = true } dashboard-auth = { workspace = true } -reqwest = { workspace = true } \ No newline at end of file +reqwest = { workspace = true } From 660c642a3ce4fcd0051ed41d643c5a0756a90901 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Mon, 12 Feb 2024 14:39:42 +0530 Subject: [PATCH 265/352] fix: better logging --- Cargo.lock | 308 ++++++++++++---------- Cargo.toml | 3 +- crates/context-aware-config/Cargo.toml | 4 +- crates/context-aware-config/src/logger.rs | 130 ++------- crates/context-aware-config/src/main.rs | 4 +- makefile | 5 + 6 files changed, 213 insertions(+), 241 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 263c7e017..4ab330310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,17 +83,17 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.3.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "ahash 0.8.3", + "ahash", "base64 0.21.2", - "bitflags 1.3.2", + "bitflags 2.3.1", "brotli", "bytes", "bytestring", @@ -166,7 +166,7 @@ dependencies = [ "futures-util", "mio", "num_cpus", - "socket2", + "socket2 0.4.9", "tokio", "tracing", ] @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.3.1" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" dependencies = [ "actix-codec", "actix-http", @@ -207,7 +207,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash", "bytes", "bytestring", "cfg-if", @@ -216,7 +216,6 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", "itoa", "language-tags", "log", @@ -228,8 +227,8 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", - "time 0.3.21", + "socket2 0.5.5", + "time", "url", ] @@ -271,17 +270,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.3" @@ -421,7 +409,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -432,7 +420,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -444,7 +432,7 @@ dependencies = [ "attribute-derive-macro", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -460,7 +448,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -691,18 +679,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.52.0", ] [[package]] @@ -765,7 +752,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -897,10 +884,8 @@ dependencies = [ "service-utils", "strum", "strum_macros", - "tracing", - "tracing-actix-web", "tracing-log", - "tracing-subscriber", + "tracing-utils", "urlencoding", "uuid", "valuable", @@ -928,7 +913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.21", + "time", "version_check", ] @@ -1029,7 +1014,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1046,7 +1031,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1109,7 +1094,7 @@ checksum = "146398d62142a0f35248a608f17edf0dde57338354966d6e41d0eb2d16980ccb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1163,7 +1148,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1172,7 +1157,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1508,7 +1493,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -1560,7 +1545,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1605,9 +1590,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.18" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -1615,7 +1600,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -1628,12 +1613,6 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.13.2" @@ -1646,7 +1625,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ - "ahash 0.8.3", + "ahash", "allocator-api2", ] @@ -1768,7 +1747,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1842,16 +1821,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.0.2" @@ -1990,7 +1959,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48354c4c4f088714424ddf090de1ff84acc82b2f08c192d46d226ae2529a465" dependencies = [ - "ahash 0.8.3", + "ahash", "anyhow", "base64 0.21.2", "bytecount", @@ -2009,7 +1978,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "time 0.3.21", + "time", "url", "uuid", ] @@ -2091,7 +2060,7 @@ dependencies = [ "futures", "getrandom", "html-escape", - "indexmap 2.0.2", + "indexmap", "itertools 0.10.5", "js-sys", "leptos_reactive", @@ -2117,13 +2086,13 @@ checksum = "a6902fabee84955a85a6cdebf8ddfbfb134091087b172e32ebb26e571d4640ca" dependencies = [ "anyhow", "camino", - "indexmap 2.0.2", + "indexmap", "parking_lot", "proc-macro2", "quote", "rstml", "serde", - "syn 2.0.32", + "syn 2.0.48", "walkdir", ] @@ -2159,7 +2128,7 @@ dependencies = [ "quote", "rstml", "server_fn_macro", - "syn 2.0.32", + "syn 2.0.48", "tracing", "uuid", ] @@ -2171,7 +2140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b64d2b4bd0ab25a4897179ee603f2fa8178da6c9f97ef3efd4fa46580fd7efc1" dependencies = [ "cfg-if", - "indexmap 2.0.2", + "indexmap", "leptos", "tracing", "wasm-bindgen", @@ -2187,7 +2156,7 @@ dependencies = [ "base64 0.21.2", "cfg-if", "futures", - "indexmap 2.0.2", + "indexmap", "js-sys", "paste", "pin-project", @@ -2255,9 +2224,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linear-map" @@ -2340,7 +2309,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -2428,10 +2397,16 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.45.0", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "native-tls" version = "0.2.11" @@ -2606,7 +2581,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -2697,7 +2672,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -2740,7 +2715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -2779,9 +2754,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -2794,16 +2769,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", "version_check", "yansi", ] [[package]] name = "quote" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2816,7 +2791,7 @@ checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" dependencies = [ "quote", "quote-use-macros", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -2828,7 +2803,7 @@ dependencies = [ "derive-where", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3005,7 +2980,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.32", + "syn 2.0.48", "syn_derive", "thiserror", ] @@ -3257,9 +3232,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -3277,13 +3252,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3297,9 +3272,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -3358,7 +3333,7 @@ dependencies = [ "serde_json", "serde_qs", "server_fn_macro_default", - "syn 2.0.32", + "syn 2.0.48", "thiserror", "xxhash-rust", ] @@ -3374,7 +3349,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.32", + "syn 2.0.48", "xxhash-rust", ] @@ -3385,7 +3360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8aaf8cf1f5dde82d3f37548732a4852f65d5279b4ae40add5a2a3c9e559f662" dependencies = [ "server_fn_macro", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3500,6 +3475,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -3634,9 +3619,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3652,7 +3637,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3695,7 +3680,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3708,17 +3693,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.21" @@ -3776,7 +3750,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.4.9", "tokio-macros", "windows-sys 0.48.0", ] @@ -3789,7 +3763,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -3856,11 +3830,12 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.6" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0b08ce08cbde6a96fc1e4ebb8132053e53ec7a5cd27eef93ede6b73ebbda06" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" dependencies = [ "actix-web", + "mutually_exclusive_features", "pin-project", "tracing", "uuid", @@ -3874,14 +3849,14 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -3929,6 +3904,25 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-utils" +version = "0.1.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#d89ab1e04e193c7fc01ef2d96e144f8a7474c04f" +dependencies = [ + "actix-http", + "actix-web", + "chrono", + "futures", + "log", + "serde", + "serde_json", + "tracing", + "tracing-actix-web", + "tracing-core", + "tracing-serde", + "tracing-subscriber", +] + [[package]] name = "treediff" version = "4.0.2" @@ -3961,7 +3955,7 @@ checksum = "f03ca4cb38206e2bef0700092660bb74d696f808514dae47fa1467cbfe26e96e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", ] [[package]] @@ -4151,12 +4145,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4184,7 +4172,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -4218,7 +4206,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4348,6 +4336,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4360,6 +4363,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4372,6 +4381,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4384,6 +4399,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4396,6 +4417,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4408,6 +4435,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4420,6 +4453,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4432,6 +4471,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.50.0" @@ -4468,20 +4513,19 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zstd" -version = "0.12.3+zstd.1.5.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" dependencies = [ - "libc", "zstd-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 0db7ea292..fa19c872d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,4 +49,5 @@ anyhow = { version = "1.0", default-features = false } strum_macros = "^0.24" strum = {version = "^0.24"} # juspay dependencies -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} \ No newline at end of file +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} +tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.1.0" } diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 6c08f48a1..d033104de 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -47,10 +47,7 @@ json-patch = { workspace = true } rand = { workspace = true } service-utils = { path = "../service-utils" } experimentation-platform = { path = "../experimentation-platform" } -tracing-actix-web = "0.7.6" tracing-log = "0.1.3" -tracing = { version = "0.1.37", features = ["valuable"]} -tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"]} valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} itertools = "0.10.5" futures = "0.3.28" @@ -66,3 +63,4 @@ actix-files = { version = "0.6" } anyhow = { workspace = true } dashboard-auth = { workspace = true } +tracing-utils = { workspace = true } \ No newline at end of file diff --git a/crates/context-aware-config/src/logger.rs b/crates/context-aware-config/src/logger.rs index 7250a67e0..d0caf0717 100644 --- a/crates/context-aware-config/src/logger.rs +++ b/crates/context-aware-config/src/logger.rs @@ -1,116 +1,42 @@ -use crate::helpers::parse_headermap_safe; +use actix_http::header::{HeaderMap, HeaderValue}; use actix_web::{ body::MessageBody, - dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - web::BytesMut, - Error, HttpMessage, + dev::{ServiceRequest, ServiceResponse}, + Error, }; -use futures::future::{ready, LocalBoxFuture, Ready}; -use futures::{FutureExt, StreamExt}; -use serde_json::{json, Value}; use service_utils::helpers::get_pod_info; -use std::{cell::RefCell, rc::Rc}; -use tracing::{span, Level, Span}; -use tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder}; -use tracing_subscriber::filter::EnvFilter; - -//---------------- GoldenSignal Middleware <start> ----------------- - -// There are two steps in middleware processing. -// 1. Middleware initialization, middleware factory gets called with -// next service in chain as parameter. -// 2. Middleware's call method gets called with normal request. - -pub struct GoldenSignalFactory; - -// Middleware factory is `Transform` trait -// `S` - type of the next service -// `B` - type of response's body -impl<S, B> Transform<S, ServiceRequest> for GoldenSignalFactory -where - S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, - S::Future: 'static, - B: 'static + std::fmt::Debug, -{ - type Response = ServiceResponse<B>; - type Error = Error; - type InitError = (); - type Transform = GoldenSignal<S>; - type Future = Ready<Result<Self::Transform, Self::InitError>>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(GoldenSignal { - service: Rc::new(RefCell::new(service)), - })) - } -} - -pub struct GoldenSignal<S> { - service: Rc<RefCell<S>>, -} - -impl<S, B> Service<ServiceRequest> for GoldenSignal<S> -where - S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, - S::Future: 'static, - B: 'static + std::fmt::Debug, -{ - type Response = ServiceResponse<B>; - type Error = Error; - type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; - - forward_ready!(service); - - fn call(&self, mut req: ServiceRequest) -> Self::Future { - let svc = self.service.clone(); - - async move { - let mut body = BytesMut::new(); - let mut payload = req.take_payload(); - while let Some(chunk) = payload.next().await { - let chunk = chunk?; - body.extend_from_slice(&chunk); - } - - let (_payload_sender, mut payload) = actix_http::h1::Payload::create(true); - payload.unread_data(body.clone().into()); - req.set_payload(payload.into()); - - let req_headers = parse_headermap_safe(req.headers()); - let res = svc.call(req).await?; - let res_headers = parse_headermap_safe(res.headers()); - let res_body = res.response().body(); - let req_body = String::from_utf8(body.freeze().to_vec()) - .and_then(|s| { - Ok(serde_json::from_str::<serde_json::Value>(s.as_str()) - .unwrap_or(Value::Null)) - }) - .unwrap_or(Value::Null); - - tracing::info!( - request_body = format!("{}", req_body), - request_headers = format!("{}", json!(req_headers)), - reponse_body = format!("{:?}", res_body), - response_headers = format!("{}", json!(res_headers)), - http.status_code = res.status().as_u16(), - "GoldenSignal", - ); - Ok(res) - } - .boxed_local() - } -} - -//---------------- GoldenSignal Middleware <end> ----------------- +use tracing_utils::tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder}; +use tracing_utils::tracing_subscriber::filter::EnvFilter; +use tracing_utils::{ + tracing::{span, Level, Span}, + tracing_actix_web, +}; pub struct CustomRootSpanBuilder; impl RootSpanBuilder for CustomRootSpanBuilder { fn on_request_start(request: &ServiceRequest) -> Span { let (pod_identifier, deployment_id) = get_pod_info(); + let headers = request.headers(); + let extractor_header = |headers: &HeaderMap, key: &str, default: &str| { + headers + .get(key) + .map(HeaderValue::to_str) + .unwrap_or(Ok(default)) + .unwrap_or(default) + .to_owned() + }; + let tenant = extractor_header(headers, "x-tenant", "no-tenant-header"); + let user_agent = extractor_header(headers, "user-agent", "no-user-agent"); + let method = request.method().to_string(); + let path = request.path(); tracing_actix_web::root_span!( request, service = "context-aware-config", + tenant, + user_agent, + method, + path, pod_id = pod_identifier, deployment_id = deployment_id ) @@ -133,10 +59,8 @@ impl RootSpanBuilder for CustomRootSpanBuilder { } } -//let custom_middleware = TracingLogger::<CustomRootSpanBuilder>::new(); - pub fn init_log_subscriber() { - let subscriber = tracing_subscriber::fmt::Subscriber::builder() + let subscriber = tracing_utils::tracing_subscriber::fmt::Subscriber::builder() .with_env_filter(EnvFilter::from_default_env()); if Ok(String::from("DEV")) == std::env::var("APP_ENV") { subscriber.compact().init(); diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 6d085f36c..6b5e7ae48 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -24,7 +24,7 @@ use tracing::{span, Level}; use snowflake::SnowflakeIdGenerator; use std::{sync::Mutex, time::Duration}; -use tracing_actix_web::TracingLogger; +use tracing_utils::{tracing_actix_web::TracingLogger, GoldenSignalFactory}; use actix_files::Files; use frontend::app::*; @@ -130,7 +130,7 @@ async fn main() -> Result<()> { .wrap(DashboardAuth::default(authenticated_routes())) .wrap(TenantMiddlewareFactory) .wrap(middlewares::cors()) - .wrap(logger::GoldenSignalFactory) + .wrap(GoldenSignalFactory) .wrap(TracingLogger::<CustomRootSpanBuilder>::new()) .app_data(Data::new(AppState { db_pool: schema_manager.clone(), diff --git a/makefile b/makefile index 6a74d463e..99ba2f1d0 100644 --- a/makefile +++ b/makefile @@ -62,6 +62,7 @@ validate-psql-connection: env-setup: + npm ci docker-compose up -d postgres localstack cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env @@ -102,6 +103,10 @@ cac: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ cargo run --color always --bin context-aware-config --no-default-features --features=ssr +dev: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo watch -x 'run --color always --bin context-aware-config --no-default-features --features=ssr' + frontend: cd crates/frontend && \ wasm-pack build --target=web --debug --no-default-features --features=hydrate From 9915af96fac776638bf55d984e87465d83da211f Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 19 Feb 2024 09:37:29 +0000 Subject: [PATCH 266/352] chore(version): v0.20.1 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 4 ++-- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464da5f67..583fd5f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.20.1 - 2024-02-19 +### Package updates +- context-aware-config bumped to context-aware-config-v0.15.2 +### Global changes +#### Bug Fixes +- [PICAF-26004] better logging - (b3d1bc8) - Kartik + +- - - + ## v0.20.0 - 2024-02-15 ### Package updates - service-utils bumped to service-utils-v0.10.3 diff --git a/Cargo.lock b/Cargo.lock index 4ab330310..63f445df3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,7 +841,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.15.1" +version = "0.15.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 08a394897..fbdaaf2dc 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.15.2 - 2024-02-19 +#### Bug Fixes +- [PICAF-26004] better logging - (b3d1bc8) - Kartik + +- - - + ## context-aware-config-v0.15.1 - 2024-02-15 #### Bug Fixes - fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index d033104de..e259b141d 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.15.1" +version = "0.15.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -63,4 +63,4 @@ actix-files = { version = "0.6" } anyhow = { workspace = true } dashboard-auth = { workspace = true } -tracing-utils = { workspace = true } \ No newline at end of file +tracing-utils = { workspace = true } From 19223be98bca56bf6f5bc0600af5249da3477e4f Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Mon, 12 Feb 2024 13:37:10 +0530 Subject: [PATCH 267/352] feat: support for service prefix --- .env.example | 3 +- Cargo.lock | 89 ++++--- Cargo.toml | 7 +- crates/context-aware-config/Cargo.toml | 8 +- crates/context-aware-config/src/auth.rs | 43 +++- crates/context-aware-config/src/main.rs | 147 +++++++---- crates/frontend/Cargo.toml | 8 +- crates/frontend/src/api.rs | 32 +-- crates/frontend/src/app.rs | 232 +++++++++++------- .../src/components/side_nav/side_nav.rs | 130 +++++----- crates/frontend/src/hoc/layout/layout.rs | 40 +-- crates/frontend/src/lib.rs | 6 +- .../src/pages/Dimensions/Dimensions.rs | 2 +- .../frontend/src/pages/Dimensions/helper.rs | 24 -- crates/frontend/src/pages/Dimensions/mod.rs | 2 - crates/frontend/src/pages/Dimensions/types.rs | 12 - crates/frontend/src/types.rs | 2 +- crates/frontend/src/utils.rs | 123 +++++++++- .../service-utils/src/middlewares/tenant.rs | 12 +- crates/service-utils/src/service/types.rs | 1 + flake.lock | 36 +-- flake.nix | 2 +- 22 files changed, 593 insertions(+), 368 deletions(-) delete mode 100644 crates/frontend/src/pages/Dimensions/helper.rs delete mode 100644 crates/frontend/src/pages/Dimensions/types.rs diff --git a/.env.example b/.env.example index 2a99f50d9..1aad4bcc2 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ DATABASE_URL=postgres://postgres:docker@dockerdns:5432/config?sslmode=disable -RUST_LOG=info +RUST_LOG=debug AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_SESSION_TOKEN=test @@ -26,4 +26,5 @@ TENANT_VALIDATION_ENABLED=false TENANTS=dev,test TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" +SERVICE_PREFIX="" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 63f445df3..8cb1b91f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -783,12 +783,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "common_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35" - [[package]] name = "config" version = "0.13.3" @@ -1071,8 +1065,8 @@ dependencies = [ [[package]] name = "dashboard-auth" -version = "0.4.1" -source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.1#bef8dff745d84cf73d7057c9dd3cd667f73cb4b2" +version = "0.4.2" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.4#3abd65d22652613bad6ac0f739fae2f2e69daef6" dependencies = [ "actix", "actix-web", @@ -1901,9 +1895,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1997,9 +1991,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "leptos" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98f0fe11faa66358ff8c2ee48881c54f8f216ecddabfc5b69cdc2e90c8e337b" +checksum = "269ba4ba91ffa73d9559c975e0be17bd4eb34c6b6abd7fdd5704106132d89d2a" dependencies = [ "cfg-if", "leptos_config", @@ -2017,9 +2011,9 @@ dependencies = [ [[package]] name = "leptos_actix" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50f8c459143ef36c6dce5786e33e48d46dfb6829af87c985d65fb3b0b402aa" +checksum = "89db4657bdcd28193e9d8cd640ec5d76b55abdf4b16cd5066f1b03f8aea49758" dependencies = [ "actix-http", "actix-web", @@ -2037,9 +2031,9 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f0e1a9a583d943b19c740c82a3ec69224c979af90f40738d93ec59ee1475bb" +checksum = "e72d8689d54737991831e9b279bb4fba36d27a93aa975c75cd4241d9a4a425ec" dependencies = [ "config", "regex", @@ -2050,9 +2044,9 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111391d1ccbc3355344f90f0893f4137db13a7f98d53fede0a3613c522ebaf19" +checksum = "ad314950d41acb1bfdb8b5924811b2983484a8d6f69a20b834a173a682657ed4" dependencies = [ "async-recursion", "cfg-if", @@ -2061,7 +2055,7 @@ dependencies = [ "getrandom", "html-escape", "indexmap", - "itertools 0.10.5", + "itertools 0.12.1", "js-sys", "leptos_reactive", "once_cell", @@ -2080,9 +2074,9 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6902fabee84955a85a6cdebf8ddfbfb134091087b172e32ebb26e571d4640ca" +checksum = "3f62dcab17728877f2d2f16d2c8a6701c4c5fbdfb4964792924acb0b50529659" dependencies = [ "anyhow", "camino", @@ -2098,9 +2092,9 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb816f3c809227b090b538994368a756d494c829b07c1bd312d07263552b8c87" +checksum = "fddda3a3b768dad90f80fb56ac6e250bc5c60755f8e3df225913aba4364ed7ee" dependencies = [ "futures", "leptos", @@ -2112,15 +2106,15 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68201041cc5af68f7eb35015336827a36c543d87dcf2403117d7244db1f14a0" +checksum = "57955d66f624265222444a5c565fea38efa5b0152a1dfc7c060a78e5fb62a852" dependencies = [ "attribute-derive", "cfg-if", "convert_case 0.6.0", "html-escape", - "itertools 0.11.0", + "itertools 0.12.1", "leptos_hot_reload", "prettyplease", "proc-macro-error", @@ -2135,9 +2129,9 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64d2b4bd0ab25a4897179ee603f2fa8178da6c9f97ef3efd4fa46580fd7efc1" +checksum = "1bc25c0f7f14ed5daf42b8d0d273ed790b0449e8ba8cff1c2fa800dc90a75acb" dependencies = [ "cfg-if", "indexmap", @@ -2149,9 +2143,9 @@ dependencies = [ [[package]] name = "leptos_reactive" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282e84ae3e3eb30ab1eb1c881bfeea8a3cb6d6c683dc99f26f2f69ee240b148d" +checksum = "b4f54a525a0edfc8c2bf3ee92aae411800b8b10892c9cd819f8e8a6d4f0d62f3" dependencies = [ "base64 0.21.2", "cfg-if", @@ -2176,15 +2170,14 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ca4422fdfba8af03d347d346f9364a4393ad36e227a018567192395285cf65" +checksum = "b31087173c60e25c329a1c6786756dd9ee97750b378622df4d780db160a09040" dependencies = [ "cached", "cfg-if", - "common_macros", "gloo-net", - "itertools 0.11.0", + "itertools 0.12.1", "js-sys", "lazy_static", "leptos", @@ -2208,9 +2201,9 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67f3810352bab860bcfa85f1760de4bd6e82cd72b14a97779d9168d37661bbf" +checksum = "2fd1517c2024bc47d764e96053e55b927f8a2159e735a0cc47232542b493df9d" dependencies = [ "inventory", "lazy_static", @@ -3315,9 +3308,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0186f969a1f9572af27159b8273252abf9a6a38934130fe6f3ae0e439d48cf14" +checksum = "6c265de965fe48e09ad8899d0ab1ffebdfa1a9914e4de5ff107b07bd94cf7541" dependencies = [ "ciborium", "const_format", @@ -3340,9 +3333,9 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbc70e4f185ff2b5c11f02a91baf830f33e456e0571d0680d1d76999ed242ed" +checksum = "f77000541a62ceeec01eef3ee0f86c155c33dac5fae750ad04a40852c6d5469a" dependencies = [ "const_format", "proc-macro-error", @@ -3355,9 +3348,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.5.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aaf8cf1f5dde82d3f37548732a4852f65d5279b4ae40add5a2a3c9e559f662" +checksum = "8a3353f22e2bcc451074d4feaa37317d9d17dff11d4311928384734ea17ab9ca" dependencies = [ "server_fn_macro", "syn 2.0.48", @@ -3940,18 +3933,18 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typed-builder" -version = "0.16.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34085c17941e36627a879208083e25d357243812c30e7d7387c3b954f30ade16" +checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.16.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03ca4cb38206e2bef0700092660bb74d696f808514dae47fa1467cbfe26e96e" +checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" dependencies = [ "proc-macro2", "quote", @@ -4178,9 +4171,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.35" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index fa19c872d..f9c007fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ assets-dir = "crates/frontend/assets" [workspace.dependencies] dotenv = "0.15.0" actix = "0.13.0" -actix-web = "4.0.0" +actix-web = "4.5.0" diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } env_logger = "0.8" log = { version="0.4.20", features = ["kv_unstable_serde"] } @@ -49,5 +49,8 @@ anyhow = { version = "1.0", default-features = false } strum_macros = "^0.24" strum = {version = "^0.24"} # juspay dependencies -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.1"} +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.4"} tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.1.0" } +leptos = { version = "0.5.2" } +leptos_meta = { version = "0.5.2" } +leptos_router = { version = "0.5.2" } \ No newline at end of file diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index e259b141d..fe38b51ee 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -56,11 +56,11 @@ futures-util = "0.3.28" external = { path = "../external" } actix-cors = "0.6.4" leptos_actix = { version = "0.5.2" } -leptos = { version = "0.5.2" } -leptos_meta = { version = "0.5.2" } -leptos_router = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } actix-files = { version = "0.6" } anyhow = { workspace = true } dashboard-auth = { workspace = true } -tracing-utils = { workspace = true } +tracing-utils = { workspace = true } \ No newline at end of file diff --git a/crates/context-aware-config/src/auth.rs b/crates/context-aware-config/src/auth.rs index 967a7ef7c..0e3f1fc3d 100644 --- a/crates/context-aware-config/src/auth.rs +++ b/crates/context-aware-config/src/auth.rs @@ -1,45 +1,62 @@ +use dashboard_auth::types::AuthenticatedRoute; + +pub fn fill_service_prefix( + routes: Vec<(&'static str, AuthenticatedRoute)>, + service_prefix: &str, +) -> Vec<(String, AuthenticatedRoute)> { + routes + .into_iter() + .map(|route| { + ( + route.0.replace("{service_prefix}", service_prefix), + route.1.clone(), + ) + }) + .collect() +} + pub mod experiments { use dashboard_auth::types::AuthenticatedRoute; pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { Vec::from([ ( - "POST::/experiments", + "POST::{service_prefix}/experiments", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PATCH::/experiments/{experiment_id}/conclude", + "PATCH::{service_prefix}/experiments/{experiment_id}/conclude", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PATCH::/experiments/{id}/ramp", + "PATCH::{service_prefix}/experiments/{id}/ramp", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PATCH::/experiments/{id}/stabilize", + "PATCH::{service_prefix}/experiments/{id}/stabilize", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PATCH::/experiments/{id}/revert", + "PATCH::{service_prefix}/experiments/{id}/revert", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/experiments/{id}/overrides", + "PUT::{service_prefix}/experiments/{id}/overrides", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), @@ -54,35 +71,35 @@ pub mod contexts { pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { Vec::from([ ( - "PUT::/context", + "PUT::{service_prefix}/context", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/context/move/{ctx_id}", + "PUT::{service_prefix}/context/move/{ctx_id}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "DELETE::/context/{ctx_id}", + "DELETE::{service_prefix}/context/{ctx_id}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/context/{ctx_id}", + "PUT::{service_prefix}/context/{ctx_id}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/context/bulk-operations", + "PUT::{service_prefix}/context/bulk-operations", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), @@ -96,7 +113,7 @@ pub mod default_config { use dashboard_auth::types::AuthenticatedRoute; pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { Vec::from([( - "PUT::/default-config/{key}", + "PUT::{service_prefix}/default-config/{key}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), @@ -109,7 +126,7 @@ pub mod dimension { use dashboard_auth::types::AuthenticatedRoute; pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { Vec::from([( - "PUT::/dimension", + "PUT::{service_prefix}/dimension", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 6b5e7ae48..d699d42c4 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -11,6 +11,7 @@ use crate::middlewares::{ }; use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use api::*; +use auth::fill_service_prefix; use dashboard_auth::{ middleware::DashboardAuth, types::{AuthenticatedRoute, AuthenticatedRouteList}, @@ -28,6 +29,7 @@ use tracing_utils::{tracing_actix_web::TracingLogger, GoldenSignalFactory}; use actix_files::Files; use frontend::app::*; +use frontend::types::Envs as UIEnvs; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use service_utils::{ @@ -64,12 +66,34 @@ async fn main() -> Result<()> { deployment_id = deployment_id ); let _span_entered = cac_span.enter(); + let service_prefix: String = + get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set"); + + /* + Reading from a env returns a String at best we cannot obtain a &'static str from it, + which seems logical as it not known at compiletime, and there is no straightforward way to do this. + + Leptos' Router component base prop type is &'static str, since service_prefix is of String type + we cannot give this as base value. + + This can be solved, if somehow we can tell rust that this String is going to live for entirety of the process, + here comes Box::leak() to our rescue, which keeps the value in the memory for the entire process lifetime, + this also enables to borrow the String value as &'static str . + */ + let service_prefix_str: &'static str = Box::leak(service_prefix.into_boxed_str()); + let base = match service_prefix_str { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 3); + let api_host: String = + get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") .expect("ENABLE_TENANT_AND_SCOPE is not set"); @@ -99,11 +123,6 @@ async fn main() -> Result<()> { ) .await; - let ui_redirect_path = match tenants.iter().next() { - Some(tenant) => format!("/admin/{}/resolve", tenant), - None => String::from("/admin"), - }; - /****** EXPERIMENTATION PLATFORM ENVs *********/ let allow_same_keys_overlapping_ctx: bool = @@ -119,15 +138,32 @@ async fn main() -> Result<()> { /****** EXPERIMENTATION PLATFORM ENVs *********/ /* Frontend configurations */ + let ui_redirect_path = match tenants.iter().next() { + Some(tenant) => format!("{}/admin/{}/resolve", base, tenant), + None => String::from("/admin"), + }; + + let ui_envs = UIEnvs { + service_prefix: service_prefix_str, + tenants: tenants.clone().into_iter().collect::<Vec<String>>(), + host: api_host.clone(), + }; + + let routes_ui_envs = ui_envs.clone(); + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); // Generate the list of routes in your Leptos App - let routes = generate_route_list(|| view! { <App/> }); + let routes = generate_route_list(move || { + return view! { <App app_envs={routes_ui_envs.clone()} /> }; + }); HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; + let leptos_envs = ui_envs.clone(); + let cac_host = cac_host.to_owned() + base.as_str(); App::new() - .wrap(DashboardAuth::default(authenticated_routes())) + .wrap(DashboardAuth::default(authenticated_routes(base.as_str()))) .wrap(TenantMiddlewareFactory) .wrap(middlewares::cors()) .wrap(GoldenSignalFactory) @@ -158,6 +194,7 @@ async fn main() -> Result<()> { tenants: tenants.to_owned(), tenant_middleware_exclusion_list: tenant_middleware_exclusion_list .to_owned(), + service_prefix: service_prefix_str.to_owned(), })) .wrap( actix_web::middleware::DefaultHeaders::new() @@ -166,55 +203,57 @@ async fn main() -> Result<()> { .add(("X-POD-ID", pod_identifier.clone())), ) .wrap(CookieToHeader) - .route( - "/health", - get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), - ) - /***************************** V1 Routes *****************************/ - .service( - scope("/context") - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) - .service(context::endpoints()), - ) - .service( - scope("/dimension") - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) - .service(dimension::endpoints()), - ) - .service( - scope("/default-config") - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) - .service(default_config::endpoints()), - ) - .service( - scope("/config") - .wrap(AuditHeader::new(TableName::Contexts)) - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) - .service(config::endpoints()), - ) - .service( - scope("/audit") - .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) - .service(audit_log::endpoints()), - ) - .service( - external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( - AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), - ), - ) - /***************************** UI Routes ******************************/ - .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) - // serve JS/WASM/CSS from `pkg` - .service(Files::new("/pkg", format!("{site_root}/pkg"))) - // serve other assets from the `assets` directory - .service(Files::new("/assets", format!("{site_root}"))) - // serve the favicon from /favicon.ico .service(web::redirect("/", ui_redirect_path.to_string())) - .service(favicon) .leptos_routes( leptos_options.to_owned(), routes.to_owned(), - || view! { <App/> }, + move || view! { <App app_envs={leptos_envs.clone()} /> }, + ) + .service( + scope(&base) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AuditHeader::new(TableName::Contexts)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) + /***************************** UI Routes ******************************/ + .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}"))) + // serve the favicon from /favicon.ico ) .app_data(Data::new(leptos_options.to_owned())) }) @@ -227,11 +266,11 @@ async fn main() -> Result<()> { .await } -fn authenticated_routes() -> AuthenticatedRouteList { +fn authenticated_routes(service_prefix: &str) -> AuthenticatedRouteList { let mut route_vector: Vec<(&str, AuthenticatedRoute)> = Vec::new(); route_vector.append(&mut auth::contexts::authenticated_routes()); route_vector.append(&mut auth::default_config::authenticated_routes()); route_vector.append(&mut auth::dimension::authenticated_routes()); route_vector.append(&mut auth::experiments::authenticated_routes()); - AuthenticatedRouteList::from(route_vector) + AuthenticatedRouteList::from(fill_service_prefix(route_vector, service_prefix)) } diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index ad0ef4063..0a9fff398 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -9,14 +9,14 @@ crate-type = ["cdylib", "rlib"] [dependencies] dotenv = { workspace = true } actix-files = { version = "0.6", optional = true } -actix-web = { version = "4", optional = true, features = ["macros"] } +actix-web = { version = "4.5.0", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" cfg-if = "1" http = { version = "0.2", optional = true } -leptos = { version = "0.5.2" } -leptos_meta = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } leptos_actix = { version = "0.5.2", optional = true } -leptos_router = { version = "0.5.2" } +leptos_router = { workspace = true } wasm-bindgen = "=0.2.89" reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index c4a1ee80d..642d5c701 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,13 +1,16 @@ -use leptos::{server, ServerFnError}; +use leptos::ServerFnError; -use crate::types::{ - Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, ListFilters, +use crate::{ + types::{ + Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, ListFilters, + }, + utils::use_host_server, }; -#[server(GetDimensions, "/fxn", "GetJson")] +// #[server(GetDimensions, "/fxn", "GetJson")] pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFnError> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = use_host_server(); let url = format!("{}/dimension", host); let response: Vec<Dimension> = client @@ -23,12 +26,12 @@ pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFn Ok(response) } -#[server(GetDefaultConfig, "/fxn", "GetJson")] +// #[server(GetDefaultConfig, "/fxn", "GetJson")] pub async fn fetch_default_config( tenant: String, ) -> Result<Vec<DefaultConfig>, ServerFnError> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = use_host_server(); let url = format!("{}/default-config", host); let response: Vec<DefaultConfig> = client @@ -44,13 +47,13 @@ pub async fn fetch_default_config( Ok(response) } -#[server(GetExperiments, "/fxn", "GetJson")] +// #[server(GetExperiments, "/fxn", "GetJson")] pub async fn fetch_experiments( filters: ListFilters, tenant: String, ) -> Result<ExperimentsResponse, ServerFnError> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = use_host_server(); let mut query_params = vec![]; if let Some(status) = filters.status { @@ -84,11 +87,12 @@ pub async fn fetch_experiments( Ok(response) } -#[server(GetConfig, "/fxn", "GetJson")] +// #[server(GetConfig, "/fxn", "GetJson")] pub async fn fetch_config(tenant: String) -> Result<Config, ServerFnError> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - let url = format!("{host}/config"); + let host = use_host_server(); + + let url = format!("{}/config", host); match client.get(url).header("x-tenant", tenant).send().await { Ok(response) => { let config: Config = response @@ -101,13 +105,13 @@ pub async fn fetch_config(tenant: String) -> Result<Config, ServerFnError> { } } -#[server(GetExperiment, "/fxn", "GetJson")] +// #[server(GetExperiment, "/fxn", "GetJson")] pub async fn fetch_experiment( exp_id: String, tenant: String, ) -> Result<Experiment, ServerFnError> { let client = reqwest::Client::new(); - let host = "http://localhost:8080"; + let host = use_host_server(); let url = format!("{}/experiments/{}", host, exp_id); match client.get(url).header("x-tenant", tenant).send().await { diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index e78ba30e2..12fe59645 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -1,9 +1,7 @@ -use std::env::VarError; -use std::str::FromStr; - use leptos::*; use leptos_meta::*; use leptos_router::*; +use serde_json::json; use crate::hoc::layout::layout::Layout; use crate::pages::Dimensions::Dimensions::Dimensions; @@ -11,98 +9,168 @@ use crate::pages::ExperimentList::ExperimentList::ExperimentList; use crate::pages::{ ContextOverride::ContextOverride::ContextOverride, DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, - Home::Home::Home, NotFound::NotFound::NotFound, + Home::Home::Home, }; -use crate::types::{AppEnv, Envs}; - -fn get_from_env_unsafe<F>(name: &str) -> Result<F, VarError> -where - F: FromStr, - <F as FromStr>::Err: std::fmt::Debug, -{ - std::env::var(name) - .map(|val| val.parse().unwrap()) - .map_err(|e| { - return e; - }) -} - -async fn load_envs() -> Envs { - let app_env = get_from_env_unsafe::<AppEnv>("APP_ENV").unwrap_or(AppEnv::DEV); - - let tenants = get_from_env_unsafe::<String>("TENANTS") - .unwrap_or("".into()) - .split(",") - .map(|tenant| tenant.to_string()) - .collect::<Vec<String>>(); - - let host = get_from_env_unsafe::<String>("API_HOSTNAME") - .unwrap_or(String::from("http://localhost:8080")); - - Envs { - host, - app_env, - tenants, - } -} +use crate::types::Envs; #[component] -pub fn App() -> impl IntoView { +pub fn App(app_envs: Envs) -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); - let envs_resource = create_blocking_resource(|| (), |_| load_envs()); - provide_context(envs_resource); + let service_prefix = app_envs.service_prefix; + provide_context(app_envs.clone()); view! { <html data-theme="light"> - <Stylesheet id="leptos" href="/pkg/style.css"/> - <Link rel="shortcut icon" type_="image/ico" href="/assets/favicon.ico"/> - <Link - href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" - rel="stylesheet" - /> + {move || { + let base = match service_prefix { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + let styles_href = base.to_owned() + "/pkg/style.css"; + let favicon_href = base.to_owned() + "/assets/favicon.ico"; + let wasm_href = base.to_owned() + "/pkg/frontend_bg.wasm"; + let js_href = base.to_owned() + "/pkg/frontend.js"; + let import_callback = "() => mod.hydrate()"; + view! { + <Stylesheet id="leptos" href=styles_href/> + <Link rel="shortcut icon" type_="image/ico" href=favicon_href/> + <Link + href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" + rel="stylesheet" + /> + {move || { + if base == "" { + view! {}.into_view() + } else { + view! { + <link + rel="preload" + href=wasm_href.clone() + as_="fetch" + type_="application/wasm" + crossorigin="" + /> + <link rel="modulepreload" href=js_href.clone()/> + <script type_="module"> + {format!( + r#" + function idle(c) {{ + if ('requestIdleCallback' in window) {{ + window.requestIdleCallback(c); + }} else {{ + c(); + }} + }} + idle(() => {{ + import('{js_href}') + .then(mod => {{ + mod.default('{wasm_href}').then({import_callback}); + }}) + }}); + "#, + )} + + </script> + } + .into_view() + } + }} + } + }} // sets the document title <Title text="Welcome to Context Aware Config"/> - // content for this welcome page - <Router> + <script type_="text/javascript">"__APP_ENVS=" {json!(app_envs).to_string()}</script> + <Router base=service_prefix> <body class="m-0 min-h-screen bg-gray-50 font-mono"> - <Layout> - <Routes> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/dimensions" - view=Dimensions - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/experiments" - view=ExperimentList - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/experiments/:id" - view=ExperimentPage - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/default-config" - view=DefaultConfig - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/overrides" - view=ContextOverride - /> - <Route - ssr=SsrMode::PartiallyBlocked - path="/admin/:tenant/resolve" - view=Home - /> - <Route path="/*any" view=NotFound/> - </Routes> - </Layout> + <Routes base=service_prefix.to_string()> + <Route + ssr=SsrMode::Async + path="/admin/:tenant/dimensions" + view=move || { + view! { + <Layout> + <Dimensions/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/experiments" + view=move || { + view! { + <Layout> + <ExperimentList/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/experiments/:id" + view=move || { + view! { + <Layout> + <ExperimentPage/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/default-config" + view=move || { + view! { + <Layout> + <DefaultConfig/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/overrides" + view=move || { + view! { + <Layout> + <ContextOverride/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/resolve" + view=move || { + view! { + <Layout> + <Home/> + </Layout> + } + } + /> + + // <Route + // path="/*any" + // view=move || { + // view! { + // <Layout> + // <NotFound/> + // </Layout> + // } + // } + // /> + + </Routes> </body> </Router> + </html> } } diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index 7b98275ee..d920ce2ae 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -1,16 +1,61 @@ use crate::components::nav_item::nav_item::NavItem; use crate::types::AppRoute; -use crate::utils::get_tenants; +use crate::utils::{get_tenants, use_url_base}; -use leptos::{logging::log, *}; +use leptos::*; use leptos_router::{use_location, use_navigate, A}; +use web_sys::Event; + +fn create_routes(tenant: &str) -> Vec<AppRoute> { + let base = use_url_base(); + vec![ + AppRoute { + key: format!("{base}/admin/{tenant}/experiments"), + path: format!("{base}/admin/{tenant}/experiments"), + icon: "ri-test-tube-fill".to_string(), + label: "Experiments".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/dimensions"), + path: format!("{base}/admin/{tenant}/dimensions"), + icon: "ri-ruler-2-fill".to_string(), + label: "Dimensions".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/default-config"), + path: format!("{base}/admin/{tenant}/default-config"), + icon: "ri-tools-line".to_string(), + label: "Default Config".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/overrides"), + path: format!("{base}/admin/{tenant}/overrides"), + icon: "ri-guide-fill".to_string(), + label: "Overrides".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/resolve"), + path: format!("{base}/admin/{tenant}/resolve"), + icon: "ri-equalizer-fill".to_string(), + label: "Resolve".to_string(), + }, + ] +} #[component] -pub fn side_nav() -> impl IntoView { +pub fn side_nav( + resolved_path: String, + original_path: String, + //params_map: Memo<ParamsMap>, +) -> impl IntoView { let location = use_location(); let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); - let (app_routes, set_app_routes) = create_signal(create_routes(&tenant_rs.get())); + let (app_routes, set_app_routes) = + create_signal(create_routes(tenant_rs.get().as_str())); + + let resolved_path = create_rw_signal(resolved_path); + let original_path = create_rw_signal(original_path); create_effect(move |_| { let current_path = location.pathname.get(); @@ -43,28 +88,26 @@ pub fn side_nav() -> impl IntoView { }> <select value=tenant_rs.get() - on:change=move |change| { - let new_tenant = event_target_value(&change); - let location = use_location(); - let mut path_tokens = location - .pathname - .get() - .split("/") - .into_iter() - .map(ToString::to_string) - .collect::<Vec<String>>(); - log!("{}{:?}", new_tenant, path_tokens); - path_tokens.remove(0); - path_tokens.remove(0); - path_tokens.remove(0); - log!("{}{:?}", new_tenant, path_tokens); - let nav = use_navigate(); - set_app_routes.set(create_routes(&new_tenant)); - tenant_ws.set(new_tenant.clone()); - nav( - format!("admin/{new_tenant}/{}", path_tokens.join("/")).as_str(), - Default::default(), - ); + on:change=move |event: Event| { + let selected_tenant = event_target_value(&event); + let base = use_url_base(); + let resolved_path_c = resolved_path.get().replace(&base, ""); + let original_path_c = original_path.get().replace(&base, ""); + logging::log!("ORIGINAL_PATH: {:?}", original_path_c); + let redirect_url = std::iter::zip( + original_path_c.split("/"), + resolved_path_c.split("/"), + ) + .map(|(o_token, r_token)| match o_token { + ":tenant" => selected_tenant.clone(), + _ => r_token.to_string(), + }) + .collect::<Vec<String>>() + .join("/"); + tenant_ws.set(selected_tenant.clone()); + set_app_routes.set(create_routes(selected_tenant.as_str())); + let navigate = use_navigate(); + navigate(redirect_url.as_str(), Default::default()) } class="select w-full max-w-xs shadow-md" @@ -121,38 +164,3 @@ pub fn side_nav() -> impl IntoView { </div> } } - -fn create_routes(tenant: &String) -> Vec<AppRoute> { - vec![ - AppRoute { - key: format!("admin/{}/experiments", tenant), - path: format!("admin/{}/experiments", tenant), - icon: "ri-test-tube-fill".to_string(), - label: "Experiments".to_string(), - }, - AppRoute { - key: format!("admin/{}/dimensions", tenant), - path: format!("admin/{}/dimensions", tenant), - icon: "ri-ruler-2-fill".to_string(), - label: "Dimensions".to_string(), - }, - AppRoute { - key: format!("admin/{}/default-config", tenant), - path: format!("admin/{}/default-config", tenant), - icon: "ri-tools-line".to_string(), - label: "Default Config".to_string(), - }, - AppRoute { - key: format!("admin/{}/overrides", tenant), - path: format!("admin/{}/overrides", tenant), - icon: "ri-guide-fill".to_string(), - label: "Overrides".to_string(), - }, - AppRoute { - key: format!("admin/{}/resolve", tenant), - path: format!("admin/{}/resolve", tenant), - icon: "ri-equalizer-fill".to_string(), - label: "Resolve".to_string(), - }, - ] -} diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs index 45a6ad885..a86874b9b 100644 --- a/crates/frontend/src/hoc/layout/layout.rs +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -2,29 +2,37 @@ use crate::components::side_nav::side_nav::SideNav; use leptos::*; use leptos_router::*; -#[component] -pub fn Layout(children: Children) -> impl IntoView { - let _params = use_params_map(); - let location = use_location(); +pub fn use_tenant() -> String { + let params_map = use_params_map(); + let route_context = use_route(); + logging::log!("use_route-params_map {:?}", params_map.get()); + logging::log!( + "use_route-original_path {:?}", + route_context.original_path() + ); + logging::log!("use_route-path {:?}", route_context.path()); - let tenant = match location - .pathname - .get() - .split("/") - .collect::<Vec<&str>>() - .get(2) - { - Some(s) => s.to_string(), - None => "mjos".to_string(), - }; + match params_map.get().get("tenant") { + Some(tenant) => tenant.clone(), + None => String::from("no-tenant"), + } +} - let (tenant_rs, tenant_ws) = create_signal(tenant); +#[component] +pub fn Layout(children: Children) -> impl IntoView { + let (tenant_rs, tenant_ws) = create_signal(use_tenant()); provide_context(tenant_rs); provide_context(tenant_ws); + let route_context = use_route(); + let original_path = route_context.original_path(); + let path = route_context.path(); + // let params_map = route_context.params(); + view! { <div> - <SideNav/> + <SideNav resolved_path=path original_path=original_path.to_string()/> + // params_map=params_map <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200 overflow-y-auto"> {children()} </main> diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 21371bd1d..ede8d4d93 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -7,6 +7,8 @@ pub mod types; mod utils; use cfg_if::cfg_if; +use utils::use_env; + cfg_if! { if #[cfg(feature = "hydrate")] { use wasm_bindgen::prelude::wasm_bindgen; @@ -16,10 +18,12 @@ cfg_if! { use app::*; use leptos::*; + console_error_panic_hook::set_once(); + let envs = use_env(); leptos::mount_to_body(move || { - view! { <App/> } + view! { <App app_envs={envs.clone()} /> } }); } } diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 07134f194..0662f15d8 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -11,7 +11,7 @@ use crate::utils::{close_modal, show_modal}; use leptos::*; use serde_json::{json, Map, Value}; -use crate::pages::Dimensions::helper::fetch_dimensions; +use crate::api::fetch_dimensions; #[derive(Clone, Debug, Default)] pub struct RowData { diff --git a/crates/frontend/src/pages/Dimensions/helper.rs b/crates/frontend/src/pages/Dimensions/helper.rs deleted file mode 100644 index 1f955870d..000000000 --- a/crates/frontend/src/pages/Dimensions/helper.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::vec::Vec; - -use leptos::{server, ServerFnError}; - -use super::types::Dimension; - -#[server(GetDimensions, "/fxn", "GetJson")] -pub async fn fetch_dimensions(tenant: String) -> Result<Vec<Dimension>, ServerFnError> { - let client = reqwest::Client::new(); - let host = "http://localhost:8080"; - - let url = format!("{}/dimension", host); - let response: Vec<Dimension> = client - .get(url) - .header("x-tenant", &tenant) - .send() - .await - .map_err(|e| ServerFnError::ServerError(e.to_string()))? - .json() - .await - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; - - Ok(response) -} diff --git a/crates/frontend/src/pages/Dimensions/mod.rs b/crates/frontend/src/pages/Dimensions/mod.rs index 9847bda4a..c39486264 100644 --- a/crates/frontend/src/pages/Dimensions/mod.rs +++ b/crates/frontend/src/pages/Dimensions/mod.rs @@ -1,3 +1 @@ pub mod Dimensions; -pub mod helper; -pub mod types; diff --git a/crates/frontend/src/pages/Dimensions/types.rs b/crates/frontend/src/pages/Dimensions/types.rs deleted file mode 100644 index 432fe065b..000000000 --- a/crates/frontend/src/pages/Dimensions/types.rs +++ /dev/null @@ -1,12 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Dimension { - pub dimension: String, - pub priority: i32, - pub created_at: DateTime<Utc>, - pub created_by: String, - pub schema: Value, -} diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index c1df77330..bf53a42bb 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -39,8 +39,8 @@ impl FromStr for AppEnv { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Envs { pub host: String, - pub app_env: AppEnv, pub tenants: Vec<String>, + pub service_prefix: &'static str, } /*********************** Experimentation Types ****************************************/ diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index 04a978bd7..bf93d9d89 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,3 +1,5 @@ +use std::env; + use crate::types::Envs; use leptos::*; use serde_json::Value; @@ -22,22 +24,129 @@ pub fn modal_action(name: &str, action: &str) { } } +pub fn add_prefix(o_str: &str, prefix: &str) -> String { + match prefix { + "" | "/" => o_str.to_owned(), + prefix => o_str.to_owned() + "/" + prefix, + } +} + +pub fn is_server() -> bool { + env::var("SERVER_NAME").is_ok() +} + +pub fn use_url_base() -> String { + let service_prefix = use_service_prefix(); + match service_prefix.as_str() { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + } +} + +pub fn use_host_server() -> String { + let service_prefix = use_service_prefix(); + if is_server() { + add_prefix("http://localhost:8080", &service_prefix) + } else { + get_host() + } +} + pub fn get_host() -> String { - let context = use_context::<Resource<(), Envs>>(); - context - .map_or(None, |resource| resource.get()) + let context = use_context::<Envs>(); + let service_prefix = use_service_prefix(); + let host = context .map(|ctx| ctx.host) - .unwrap_or(String::from("http://localhost:8080")) + .or_else(|| match js_sys::eval("__APP_ENVS?.host") { + Ok(value) => value + .dyn_into::<js_sys::JsString>() + .expect("host is not a string") + .as_string(), + Err(e) => { + logging::log!("Unable to fetch host from __APP_ENVS: {:?}", e); + None + } + }) + .unwrap_or(String::from("http://localhost:xxxx")); + + add_prefix(&host, &service_prefix) } pub fn get_tenants() -> Vec<String> { - let context = use_context::<Resource<(), Envs>>(); + let context = use_context::<Envs>(); context - .map_or(None, |resource| resource.get()) - .map(|ctx| ctx.tenants) + .map(|ctx: Envs| ctx.tenants) + .or_else(|| { + let tenant_value = match js_sys::eval("__APP_ENVS?.tenants") { + Ok(value) => value + .dyn_into::<js_sys::Array>() + .expect("tenants is not an array") + .to_vec() + .into_iter() + .map(|tenant| { + tenant.dyn_into::<js_sys::JsString>().ok().map(String::from) + }) + .collect::<Option<Vec<String>>>(), + Err(e) => { + logging::log!("Unable to fetch tenants from __APP_ENVS: {:?}", e); + None + } + }; + tenant_value + }) .unwrap_or(vec![]) } +pub fn use_env() -> Envs { + let context = use_context::<Envs>(); + context + .or_else(|| { + let envs = match js_sys::eval("__APP_ENVS") { + Ok(value) => { + let env_obj = value + .dyn_into::<js_sys::Object>() + .expect("__APP_ENV is not an object"); + let env_str: &'static str = Box::leak( + js_sys::JSON::stringify(&env_obj) + .ok() + .map(String::from) + .unwrap_or(String::new()) + .into_boxed_str(), + ); + let envs = serde_json::from_str::<Envs>(env_str) + .expect("unable to parse to Envs struct"); + Some(envs) + } + Err(e) => { + logging::log!("Unable to fetch __APP_ENVS: {:?}", e); + None + } + }; + envs + }) + .expect("unable to get envs") +} + +pub fn use_service_prefix() -> String { + let context = use_context::<Envs>(); + context + .map(|ctx: Envs| String::from(ctx.service_prefix)) + .or_else(|| { + let service_prefix_value = match js_sys::eval("__APP_ENV?.service_prefix") { + Ok(value) => value.dyn_into::<js_sys::JsString>().map(String::from).ok(), + Err(e) => { + logging::log!( + "Unable to fetch service_prefix from __APP_ENVS: {:?}", + e + ); + None + } + }; + service_prefix_value + }) + .unwrap_or(String::new()) +} + pub fn get_element_by_id<T>(id: &'static str) -> Option<T> where T: wasm_bindgen::JsCast + Clone, diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service-utils/src/middlewares/tenant.rs index b6ad70035..1e1755aca 100644 --- a/crates/service-utils/src/middlewares/tenant.rs +++ b/crates/service-utils/src/middlewares/tenant.rs @@ -52,6 +52,9 @@ fn extract_tenant_from_url<'a>( let pattern_segments = pattern.split("/"); let path_segments = path.split("/").collect::<Vec<&str>>(); + debug!("PATTERN_SEGMENTS ===> {:?}", pattern_segments); + debug!("PATH_SEGMENTS ===> {:?}", path_segments); + std::iter::zip(path_segments, pattern_segments) .find(|(_, pattern_seg)| pattern_seg == &"{tenant}") .map(|(path_seg, _)| path_seg) @@ -91,10 +94,15 @@ where } }; - let request_path = req.uri().path(); + let base = match app_state.service_prefix.as_str() { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + + let request_path = req.uri().path().replace(&base, ""); let is_excluded: bool = app_state .tenant_middleware_exclusion_list - .contains(request_path); + .contains(&request_path); if !is_excluded && app_state.enable_tenant_and_scope { debug!( diff --git a/crates/service-utils/src/service/types.rs b/crates/service-utils/src/service/types.rs index 579fd9801..448dd63fa 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service-utils/src/service/types.rs @@ -40,6 +40,7 @@ pub struct AppState { pub snowflake_generator: Mutex<SnowflakeIdGenerator>, pub enable_tenant_and_scope: bool, pub tenant_middleware_exclusion_list: HashSet<String>, + pub service_prefix: String, } impl FromStr for AppEnv { diff --git a/flake.lock b/flake.lock index 72732dde6..68e6b01f4 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -23,11 +23,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -56,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "lastModified": 1707743206, + "narHash": "sha256-AehgH64b28yKobC/DAWYZWkJBxL/vP83vkY+ag2Hhy4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "rev": "2d627a2a704708673e56346fcb13d25344b8eaf3", "type": "github" }, "original": { @@ -70,11 +70,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1704194953, - "narHash": "sha256-RtDKd8Mynhe5CFnVT8s0/0yqtWFMM9LmCzXv/YKxnq4=", + "lastModified": 1707689078, + "narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bd645e8668ec6612439a9ee7e71f7eac4099d4f6", + "rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8", "type": "github" }, "original": { @@ -86,11 +86,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1681358109, - "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "lastModified": 1706487304, + "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", "type": "github" }, "original": { @@ -114,11 +114,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1704420966, - "narHash": "sha256-oy7QoQ9/5ws9t+S7f4uGqURAKxMdu/CrBJPAXVpFmc0=", + "lastModified": 1707790272, + "narHash": "sha256-KQXPNl3BLdRbz7xx+mwIq/017fxLRk6JhXHxVWCKsTU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "9fe08ffa92ee9166c93ad34c27a4792f0c0d58a6", + "rev": "8dfbe2dffc28c1a18a29ffa34d5d0b269622b158", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 11f240a6e..8290821da 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,7 @@ leptosfmt wasm-pack curl - ( rust-bin.stable.latest.default.override { + ( rust-bin.stable."1.73.0".default.override { extensions = [ "rust-src" ]; targets = [ "wasm32-unknown-unknown" ]; }) From 2ac33ed935e748485421876a175723decf6e1258 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Tue, 20 Feb 2024 09:27:49 +0000 Subject: [PATCH 268/352] chore(version): v0.21.0 [skip ci] --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 4 ++-- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 583fd5f9e..301306a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.21.0 - 2024-02-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.16.0 +- service-utils bumped to service-utils-v0.11.0 +### Global changes +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + ## v0.20.1 - 2024-02-19 ### Package updates - context-aware-config bumped to context-aware-config-v0.15.2 diff --git a/Cargo.lock b/Cargo.lock index 8cb1b91f9..9d9fa85e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -835,7 +835,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.15.2" +version = "0.16.0" dependencies = [ "actix", "actix-cors", @@ -3358,7 +3358,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.10.3" +version = "0.11.0" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index fbdaaf2dc..e60c07135 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.16.0 - 2024-02-20 +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.15.2 - 2024-02-19 #### Bug Fixes - [PICAF-26004] better logging - (b3d1bc8) - Kartik diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index fe38b51ee..d240a52b9 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.15.2" +version = "0.16.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -63,4 +63,4 @@ actix-files = { version = "0.6" } anyhow = { workspace = true } dashboard-auth = { workspace = true } -tracing-utils = { workspace = true } \ No newline at end of file +tracing-utils = { workspace = true } diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 4cc650202..fa49e8f3c 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.11.0 - 2024-02-20 +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.10.3 - 2024-02-15 #### Bug Fixes - fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 252ca1c0c..f1635d904 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.10.3" +version = "0.11.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From b01811772cb91c66e2529a3a6c1aab72ef7d9af4 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 21 Feb 2024 13:04:01 +0530 Subject: [PATCH 269/352] fix: using SERVICE_NAME in is_server instead of SERVER_NAME(wrong var name) --- crates/frontend/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index bf93d9d89..942439222 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -32,7 +32,7 @@ pub fn add_prefix(o_str: &str, prefix: &str) -> String { } pub fn is_server() -> bool { - env::var("SERVER_NAME").is_ok() + env::var("SERVICE_NAME").is_ok() } pub fn use_url_base() -> String { From f4e4a31aa6cbb6c4e74bc05a6ab90c1358422738 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 21 Feb 2024 08:21:48 +0000 Subject: [PATCH 270/352] chore(version): v0.22.0 [skip ci] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 301306a2a..49f722eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.22.0 - 2024-02-21 +### Packages +- context-aware-config locked to context-aware-config-v0.16.0 +- cac_client locked to cac_client-v0.5.0 +- superposition_client locked to superposition_client-v0.4.0 +- service-utils locked to service-utils-v0.11.0 +- external locked to external-v0.3.0 +- experimentation-platform locked to experimentation-platform-v0.9.3 +### Global changes +#### Bug Fixes +- using SERVICE_NAME in is_server instead of SERVER_NAME(wrong var name) - (efe97f0) - Shubhranshu Sanjeev + +- - - + ## v0.21.0 - 2024-02-20 ### Package updates - context-aware-config bumped to context-aware-config-v0.16.0 From 3786b698fcc6c0a13213d3eda5a506ec89afcc5f Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Fri, 9 Feb 2024 16:11:40 +0530 Subject: [PATCH 271/352] feat: js eval with node exec --- Dockerfile | 4 + crates/context-aware-config/package-lock.json | 1070 +++++++++++++++++ crates/context-aware-config/package.json | 11 + crates/context-aware-config/src/lib.rs | 1 + crates/context-aware-config/src/main.rs | 1 + .../src/validation_functions.rs | 104 ++ .../context-aware-config/tests/cac_tests.rs | 38 + .../{test => tests}/index.js | 0 libs/js/package.json | 2 +- makefile | 4 + 10 files changed, 1234 insertions(+), 1 deletion(-) create mode 100644 crates/context-aware-config/package-lock.json create mode 100644 crates/context-aware-config/package.json create mode 100644 crates/context-aware-config/src/validation_functions.rs create mode 100644 crates/context-aware-config/tests/cac_tests.rs rename crates/context-aware-config/{test => tests}/index.js (100%) diff --git a/Dockerfile b/Dockerfile index 3f6868d3b..21e446717 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,9 @@ COPY . . RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh RUN npm ci --loglevel=info +RUN cd crates/context-aware-config/ && npm ci +RUN mkdir -p target/node_modules +RUN cp -a crates/context-aware-config/node_modules target/node_modules/ # building frontend RUN --mount=type=ssh cd crates/frontend \ @@ -43,6 +46,7 @@ RUN apt-get update && apt-get install -y libpq5 ca-certificates COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config COPY --from=builder /build/Cargo.toml /app/Cargo.toml COPY --from=builder /build/target/site /app/target/site +COPY --from=builder /build/target/node_modules /app/target/node_modules ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT CMD ["/app/context-aware-config"] \ No newline at end of file diff --git a/crates/context-aware-config/package-lock.json b/crates/context-aware-config/package-lock.json new file mode 100644 index 000000000..61fc6b17d --- /dev/null +++ b/crates/context-aware-config/package-lock.json @@ -0,0 +1,1070 @@ +{ + "name": "context-aware-config", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "context-aware-config", + "version": "0.0.1", + "dependencies": { + "axios": "^0.16.1", + "eslint": "^8.56.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha512-IMYFDrcVbUksQhsMYtWCM6KdNaDpr1NY56dpzaIgj92ecPVI29bf2sOgAf8aGTiq8UoixJD61Pj0Ahej5DPv7w==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "^1.2.3", + "is-buffer": "^1.1.5" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/crates/context-aware-config/package.json b/crates/context-aware-config/package.json new file mode 100644 index 000000000..621cf3646 --- /dev/null +++ b/crates/context-aware-config/package.json @@ -0,0 +1,11 @@ +{ + "name": "context-aware-config", + "version": "0.0.1", + "private": true, + "description": "dependencies for CAC function validators", + "dependencies": { + "axios": "^0.16.1", + "eslint": "^8.56.0" + + } +} diff --git a/crates/context-aware-config/src/lib.rs b/crates/context-aware-config/src/lib.rs index 851963c56..c3971d3a1 100644 --- a/crates/context-aware-config/src/lib.rs +++ b/crates/context-aware-config/src/lib.rs @@ -2,3 +2,4 @@ pub mod api; pub mod db; pub mod helpers; pub mod middlewares; +pub mod validation_functions; diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index d699d42c4..51b06b52e 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -4,6 +4,7 @@ mod db; mod helpers; mod logger; mod middlewares; +mod validation_functions; use crate::middlewares::{ audit_response_header::{AuditHeader, TableName}, diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs new file mode 100644 index 000000000..425a7e757 --- /dev/null +++ b/crates/context-aware-config/src/validation_functions.rs @@ -0,0 +1,104 @@ +use serde_json::{json, Value}; +use std::process::Command; +use std::str; + +const IMPORT_CODE: &str = r#" + /*eslint no-unused-vars: "off"*/ + /*eslint no-extra-semi: "off"*/ + const axios = require("/target/node_modules/axios"); + "#; + +const EXIT_LOGIC_CODE: &str = r#" + if (fun_value != true) { + process.exit(1); + }; + "#; + +const ES_LINT_CODE: &str = r#" + const eslint = require("eslint"); + const linter = new eslint.ESLint({ + useEslintrc: false, + overrideConfig: { + extends: ["eslint:recommended"], + parserOptions: { + sourceType: "module", + ecmaVersion: "latest", + }, + env: { + browser: true, + commonjs: true, + } + }, + }); + + linter.lintText(codeToLint).then((results) => { + var err_count = 0; + for (var err_obj of results) { + err_count = err_count + err_obj.errorCount; + } + + if (err_count > 0) { + process.exit(1); + } + }).catch((error) => { + process.exit(1); + + }); + + "#; + +fn runtime_wrapper(function_name: &str, value: Value) -> String { + let fun_call: String = format!("const fun_value = {}({});", function_name, value); + fun_call + EXIT_LOGIC_CODE +} + +pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> bool { + let output = Command::new("node") + .arg("-e") + .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) + .output(); + log::trace!("{}", format!("validation function output : {:?}", output)); + match output { + Ok(val) => { + if !(val.status.success()) { + let stderr = str::from_utf8(&val.stderr) + .unwrap_or("[Invalid UTF-8 in stderr]") + .to_owned(); + log::error!("{}", format!("validation function output : {:?}", stderr)); + } + val.status.success() + } + Err(e) => { + log::error!("js_eval error: {}", e); + false + } + } +} + +fn eslint_logic(code_str: &str) -> String { + let code = IMPORT_CODE.to_string() + code_str; + let fun_call: String = format!("\nconst codeToLint = {};", json!(code)); + fun_call + ES_LINT_CODE +} + +pub fn compile_fn(code_str: &str) -> bool { + let output = Command::new("node") + .arg("-e") + .arg(eslint_logic(code_str)) + .output(); + match output { + Ok(val) => { + if !(val.status.success()) { + let stderr = str::from_utf8(&val.stderr) + .unwrap_or("[Invalid UTF-8 in stderr]") + .to_owned(); + log::error!("{}", format!("eslint check output : {:?}", stderr)); + } + val.status.success() + } + Err(e) => { + log::error!("eslint check error: {}", e); + false + } + } +} diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs new file mode 100644 index 000000000..cf34caa48 --- /dev/null +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -0,0 +1,38 @@ +use context_aware_config::validation_functions::{compile_fn, execute_fn}; +use serde_json::json; + +// #[test] //todo : currently there is issue in running this test +fn test_execute_fn() { + let code_ok = r#" + function test_fun() { + return true; + }; + "#; + + let execute_code_error = r#" + function test_fun() { + return false; + } + "#; + + let compile_code_error = r#" + function test_fun( { + return true; + } + "#; + + assert_eq!( + execute_fn(&(code_ok.to_owned()), &"test_fun".to_owned(), json!(10)), + true + ); + assert_eq!( + execute_fn( + &(execute_code_error.to_owned()), + &"test_fun".to_owned(), + json!(10) + ), + false + ); + assert_eq!(compile_fn(&(code_ok.to_owned())), true); + assert_eq!(compile_fn(&(compile_code_error.to_owned())), true); +} diff --git a/crates/context-aware-config/test/index.js b/crates/context-aware-config/tests/index.js similarity index 100% rename from crates/context-aware-config/test/index.js rename to crates/context-aware-config/tests/index.js diff --git a/libs/js/package.json b/libs/js/package.json index c7c4e467a..caa279b00 100644 --- a/libs/js/package.json +++ b/libs/js/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "node ./test/index.js", + "test": "node ./tests/index.js", "compile": "npx tsc", "dev": "tsc --project tsconfig.json", "buildLib": "webpack --mode=production", diff --git a/makefile b/makefile index 99ba2f1d0..b4811169a 100644 --- a/makefile +++ b/makefile @@ -118,6 +118,10 @@ frontend: cp -a crates/frontend/assets/. target/site/ backend: + -rm -rf target/node_modules + npm --prefix ./crates/context-aware-config/ ci + mkdir target/node_modules + mv crates/context-aware-config/node_modules target/node_modules cargo build --color always build: frontend backend From 267dc4abf181d470dec681fdb7810dd7b711eae4 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 22 Feb 2024 06:29:27 +0000 Subject: [PATCH 272/352] chore(version): v0.23.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f722eea..38ae395f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.23.0 - 2024-02-22 +### Package updates +- context-aware-config bumped to context-aware-config-v0.17.0 +### Global changes +#### Features +- [PICAF-25877] js eval with node exec - (adc9b19) - Pratik Mishra + +- - - + ## v0.22.0 - 2024-02-21 ### Packages - context-aware-config locked to context-aware-config-v0.16.0 diff --git a/Cargo.lock b/Cargo.lock index 9d9fa85e6..4dce85ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -835,7 +835,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.16.0" +version = "0.17.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index e60c07135..b6411d8bb 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.17.0 - 2024-02-22 +#### Features +- [PICAF-25877] js eval with node exec - (adc9b19) - Pratik Mishra + +- - - + ## context-aware-config-v0.16.0 - 2024-02-20 #### Features - support for service prefix - (a2915b4) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index d240a52b9..51d7dd60e 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.16.0" +version = "0.17.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 0bb238e26a2ea4a72063c90e2b7a25b44ea995e5 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Thu, 22 Feb 2024 15:24:30 +0530 Subject: [PATCH 273/352] feat: CRUD APIs for function validator --- .../2024-02-19-125126_functions/down.sql | 1 + .../2024-02-19-125126_functions/up.sql | 19 + .../src/api/functions/handlers.rs | 233 +++++ .../src/api/functions/helpers.rs | 48 + .../src/api/functions/mod.rs | 4 + .../src/api/functions/types.rs | 27 + crates/context-aware-config/src/api/mod.rs | 1 + crates/context-aware-config/src/auth.rs | 43 + crates/context-aware-config/src/db/models.rs | 18 +- crates/context-aware-config/src/db/schema.rs | 18 + crates/context-aware-config/src/main.rs | 6 + .../src/validation_functions.rs | 23 +- .../context-aware-config/tests/cac_tests.rs | 27 +- package-lock.json | 839 +++++++++++++++++- package.json | 4 + 15 files changed, 1247 insertions(+), 64 deletions(-) create mode 100644 crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql create mode 100644 crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql create mode 100644 crates/context-aware-config/src/api/functions/handlers.rs create mode 100644 crates/context-aware-config/src/api/functions/helpers.rs create mode 100644 crates/context-aware-config/src/api/functions/mod.rs create mode 100644 crates/context-aware-config/src/api/functions/types.rs diff --git a/crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql b/crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql b/crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql new file mode 100644 index 000000000..73c8c0cbc --- /dev/null +++ b/crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here +-- Name: functions; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.functions ( + function_name text PRIMARY KEY, + published_code text, + draft_code text NOT NULL, + function_description text NOT NULL, + published_runtime_version VARCHAR(16), + draft_runtime_version VARCHAR(16) NOT NULL, + published_at timestamp without time zone, + draft_edited_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + published_by text, + draft_edited_by text NOT NULL +); +-- +-- Name: functions functions_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER functions_audit AFTER INSERT OR DELETE OR UPDATE ON public.functions FOR EACH ROW EXECUTE FUNCTION public.event_logger(); \ No newline at end of file diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs new file mode 100644 index 000000000..2bec7e6a4 --- /dev/null +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -0,0 +1,233 @@ +extern crate base64; +use base64::prelude::*; + +use super::helpers::{decode_function, fetch_function}; + +use crate::{ + db::{ + self, + models::Function, + schema::functions::{dsl::functions, function_name}, + }, + validation_functions, +}; +use actix_web::{ + delete, + error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, + get, patch, post, + web::{self, Json}, + HttpResponse, Result, Scope, +}; +use chrono::Utc; +use dashboard_auth::types::User; +use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::json; +use service_utils::service::types::DbConnection; +use validation_functions::compile_fn; + +use super::types::{CreateFunctionRequest, UpdateFunctionRequest}; + +pub fn endpoints() -> Scope { + Scope::new("") + .service(create) + .service(update) + .service(get) + .service(list_functions) + .service(delete_function) +} + +#[post("")] +async fn create( + user: User, + request: web::Json<CreateFunctionRequest>, + db_conn: DbConnection, +) -> Result<Json<Function>> { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + + if let Err(e) = compile_fn(&req.function.to_string()) { + return Err(ErrorBadRequest(json!({ "message": e }))); + } + + let function = Function { + function_name: req.function_name, + draft_code: BASE64_STANDARD.encode(req.function), + draft_runtime_version: req.runtime_version, + draft_edited_by: user.email, + draft_edited_at: Utc::now().naive_utc(), + published_code: None, + published_at: None, + published_by: None, + published_runtime_version: None, + function_description: req.description, + }; + + let insert: Result<Function, diesel::result::Error> = diesel::insert_into(functions) + .values(&function) + .get_result(&mut conn); + + match insert { + Ok(mut res) => { + decode_function(&mut res)?; + Ok(Json(res)) + } + Err(e) => match e { + diesel::result::Error::DatabaseError(kind, e) => { + log::info!("Function error: {:?}", e); + match kind { + diesel::result::DatabaseErrorKind::UniqueViolation => { + return Err(ErrorBadRequest( + json!({"message": "Function already exists."}), + )) + } + _ => { + return Err(ErrorBadRequest( + json!({"message": "An error occured please contact the admin"}), + )) + } + } + } + _ => { + log::info!("Function creation failed with error: {e}"); + return Err(ErrorInternalServerError( + json!({"message": "An error occured please contact the admin."}), + )); + } + }, + } +} + +#[patch("/{function_name}")] +async fn update( + user: User, + params: web::Path<String>, + request: web::Json<UpdateFunctionRequest>, + db_conn: DbConnection, +) -> Result<Json<Function>> { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + let f_name = params.into_inner(); + + let result = match fetch_function(&f_name, &mut conn) { + Ok(val) => val, + Err(diesel::result::Error::NotFound) => { + log::info!("Function not found."); + return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + } + Err(e) => { + log::info!("Failed to update Function with error: {e}"); + return Err(ErrorInternalServerError( + json!({"message": "Failed to update Function"}), + )); + } + }; + + // Function Linter Check + if let Some(function) = &req.function { + if let Err(e) = compile_fn(function) { + return Err(ErrorBadRequest(json!({ "message": e }))); + } + } + + let new_function = Function { + function_name: f_name.to_owned(), + draft_code: req.function.map_or_else( + || result.draft_code.clone(), + |func| BASE64_STANDARD.encode(func), + ), + draft_runtime_version: req + .runtime_version + .unwrap_or(result.draft_runtime_version), + function_description: req.description.unwrap_or(result.function_description), + draft_edited_by: user.email, + draft_edited_at: Utc::now().naive_utc(), + published_code: result.published_code, + published_at: result.published_at, + published_by: result.published_by, + published_runtime_version: result.published_runtime_version, + }; + + let update: Result<Function, diesel::result::Error> = diesel::update(functions) + .filter(db::schema::functions::function_name.eq(f_name)) + .set(new_function) + .get_result(&mut conn); + + match update { + Ok(mut res) => { + decode_function(&mut res)?; + Ok(Json(res)) + } + Err(e) => { + log::info!("Function updation failed with error: {e}"); + Err(ErrorInternalServerError( + json!({"message": "Failed to update Function"}), + )) + } + } +} + +#[get("/{function_name}")] +async fn get(params: web::Path<String>, db_conn: DbConnection) -> Result<HttpResponse> { + let DbConnection(mut conn) = db_conn; + let f_name = params.into_inner(); + let result = fetch_function(&f_name, &mut conn); + + match result { + Ok(mut function) => { + decode_function(&mut function)?; + Ok(HttpResponse::Ok().json(function)) + } + Err(e) => { + log::info!("Error getting function: {e}"); + Err(ErrorInternalServerError( + json!({"message": "Function does not exists."}), + )) + } + } +} + +#[get("")] +async fn list_functions(db_conn: DbConnection) -> Result<Json<Vec<Function>>> { + let DbConnection(mut conn) = db_conn; + let result: Result<Vec<Function>, diesel::result::Error> = + functions.get_results(&mut conn); + + match result { + Ok(mut function_list) => { + for function in function_list.iter_mut() { + decode_function(function)?; + } + Ok(Json(function_list)) + } + Err(e) => { + log::info!("Error getting the functions: {e}"); + Err(ErrorInternalServerError( + json!({"message": "Error getting the functions."}), + )) + } + } +} + +#[delete("/{function_name}")] +async fn delete_function( + user: User, + params: web::Path<String>, + db_conn: DbConnection, +) -> Result<HttpResponse> { + let DbConnection(mut conn) = db_conn; + let f_name = params.into_inner(); + + let deleted_row = + delete(functions.filter(function_name.eq(&f_name))).execute(&mut conn); + match deleted_row { + Ok(0) => Err(ErrorNotFound(json!({"message": "Function not found."}))), + Ok(_) => { + log::info!("{f_name} function deleted by {}", user.email); + Ok(HttpResponse::NoContent().finish()) + } + Err(e) => { + log::error!("function delete query failed with error: {e}"); + Err(ErrorInternalServerError("")) + } + } +} diff --git a/crates/context-aware-config/src/api/functions/helpers.rs b/crates/context-aware-config/src/api/functions/helpers.rs new file mode 100644 index 000000000..52350363c --- /dev/null +++ b/crates/context-aware-config/src/api/functions/helpers.rs @@ -0,0 +1,48 @@ +extern crate base64; +use actix_web::error::ErrorInternalServerError; +use base64::prelude::*; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + result::Error, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use serde_json::json; +use std::str; + +use crate::db::{self, models::Function, schema::functions::dsl::functions}; + +pub fn fetch_function( + f_name: &String, + conn: &mut PooledConnection<ConnectionManager<PgConnection>>, +) -> actix_web::Result<Function, Error> { + Ok(functions + .filter(db::schema::functions::function_name.eq(f_name)) + .get_result::<Function>(conn)?) +} + +pub fn decode_function(function: &mut Function) -> actix_web::Result<()> { + function.draft_code = decode_base64_to_string(&function.draft_code)?; + if let Some(code) = &function.published_code { + function.published_code = Some(decode_base64_to_string(&code)?); + } + Ok(()) +} + +pub fn decode_base64_to_string(code: &String) -> actix_web::Result<String> { + BASE64_STANDARD + .decode(code) + .map_err(|e| { + log::info!("Error while decoding function: {}", e); + ErrorInternalServerError(json!({"message": "Failed to decode function"})) + }) + .and_then(|decoded_code| { + str::from_utf8(&decoded_code) + .map(|res| res.to_string()) + .map_err(|e| { + log::info!("Error while decoding function: {}", e); + ErrorInternalServerError( + json!({"message": "Failed to decode function"}), + ) + }) + }) +} diff --git a/crates/context-aware-config/src/api/functions/mod.rs b/crates/context-aware-config/src/api/functions/mod.rs new file mode 100644 index 000000000..8ebb62780 --- /dev/null +++ b/crates/context-aware-config/src/api/functions/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +pub use handlers::endpoints; +pub mod helpers; +mod types; diff --git a/crates/context-aware-config/src/api/functions/types.rs b/crates/context-aware-config/src/api/functions/types.rs new file mode 100644 index 000000000..10f779f4e --- /dev/null +++ b/crates/context-aware-config/src/api/functions/types.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct UpdateFunctionRequest { + pub function: Option<String>, + pub runtime_version: Option<String>, + pub description: Option<String>, +} + +#[derive(Debug, Deserialize)] +pub struct CreateFunctionRequest { + pub function_name: String, + pub function: String, + pub runtime_version: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionResponse { + pub function_name: String, + pub function: String, + pub function_description: String, + pub runtime_version: String, + pub status: String, + pub published_at: String, + pub drafted_at: String, +} diff --git a/crates/context-aware-config/src/api/mod.rs b/crates/context-aware-config/src/api/mod.rs index a247540be..6616da3b8 100644 --- a/crates/context-aware-config/src/api/mod.rs +++ b/crates/context-aware-config/src/api/mod.rs @@ -3,3 +3,4 @@ pub mod config; pub mod context; pub mod default_config; pub mod dimension; +pub mod functions; diff --git a/crates/context-aware-config/src/auth.rs b/crates/context-aware-config/src/auth.rs index 0e3f1fc3d..f3c0f9d67 100644 --- a/crates/context-aware-config/src/auth.rs +++ b/crates/context-aware-config/src/auth.rs @@ -134,3 +134,46 @@ pub mod dimension { )]) } } + +pub mod functions { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "POST::/function", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "GET::/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "DELETE::/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "GET::/function", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 1a752ea81..9d7c1f7f2 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -1,4 +1,4 @@ -use crate::db::schema::{contexts, default_configs, dimensions, event_log}; +use crate::db::schema::{contexts, default_configs, dimensions, event_log, functions}; use chrono::{offset::Utc, DateTime, NaiveDateTime}; use diesel::{AsChangeset, Insertable, Queryable, Selectable}; use serde::Serialize; @@ -40,6 +40,22 @@ pub struct DefaultConfig { pub schema: Value, } +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(name))] +pub struct Function { + pub function_name: String, + pub published_code: Option<String>, + pub draft_code: String, + pub function_description: String, + pub published_runtime_version: Option<String>, + pub draft_runtime_version: String, + pub published_at: Option<NaiveDateTime>, + pub draft_edited_at: NaiveDateTime, + pub published_by: Option<String>, + pub draft_edited_by: String, +} + #[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(table_name = event_log)] diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 441e4b283..4a842675b 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -579,6 +579,23 @@ diesel::table! { } } +diesel::table! { + functions (function_name) { + function_name -> Text, + published_code -> Nullable<Text>, + draft_code -> Text, + function_description -> Text, + #[max_length = 16] + published_runtime_version -> Nullable<Varchar>, + #[max_length = 16] + draft_runtime_version -> Varchar, + published_at -> Nullable<Timestamp>, + draft_edited_at -> Timestamp, + published_by -> Nullable<Text>, + draft_edited_by -> Text, + } +} + diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, @@ -625,4 +642,5 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m10, event_log_y2026m11, event_log_y2026m12, + functions, ); diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 51b06b52e..083767227 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -243,6 +243,11 @@ async fn main() -> Result<()> { .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(audit_log::endpoints()), ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) .service( external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), @@ -273,5 +278,6 @@ fn authenticated_routes(service_prefix: &str) -> AuthenticatedRouteList { route_vector.append(&mut auth::default_config::authenticated_routes()); route_vector.append(&mut auth::dimension::authenticated_routes()); route_vector.append(&mut auth::experiments::authenticated_routes()); + route_vector.append(&mut auth::functions::authenticated_routes()); AuthenticatedRouteList::from(fill_service_prefix(route_vector, service_prefix)) } diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 425a7e757..1f5c78855 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -52,7 +52,7 @@ fn runtime_wrapper(function_name: &str, value: Value) -> String { fun_call + EXIT_LOGIC_CODE } -pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> bool { +pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), String> { let output = Command::new("node") .arg("-e") .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) @@ -64,13 +64,18 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> bool { let stderr = str::from_utf8(&val.stderr) .unwrap_or("[Invalid UTF-8 in stderr]") .to_owned(); - log::error!("{}", format!("validation function output : {:?}", stderr)); + log::error!( + "{}", + format!("validation function output error: {:?}", stderr) + ); + Err(stderr) + } else { + Ok(()) } - val.status.success() } Err(e) => { log::error!("js_eval error: {}", e); - false + Err(format!("js_eval error: {}", e)) } } } @@ -81,7 +86,7 @@ fn eslint_logic(code_str: &str) -> String { fun_call + ES_LINT_CODE } -pub fn compile_fn(code_str: &str) -> bool { +pub fn compile_fn(code_str: &str) -> Result<(), String> { let output = Command::new("node") .arg("-e") .arg(eslint_logic(code_str)) @@ -92,13 +97,15 @@ pub fn compile_fn(code_str: &str) -> bool { let stderr = str::from_utf8(&val.stderr) .unwrap_or("[Invalid UTF-8 in stderr]") .to_owned(); - log::error!("{}", format!("eslint check output : {:?}", stderr)); + log::error!("{}", format!("eslint check output error: {:?}", stderr)); + Err(stderr) + } else { + Ok(()) } - val.status.success() } Err(e) => { log::error!("eslint check error: {}", e); - false + Err(format!("js_eval error: {}", e)) } } } diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index cf34caa48..192a942a6 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -21,18 +21,23 @@ fn test_execute_fn() { } "#; + let err_execute = match execute_fn( + &(execute_code_error.to_owned()), + &"test_fun".to_owned(), + json!(10), + ) { + Ok(()) => false, + Err(e) => e.contains("Bad schema"), + }; + let err_compile = match compile_fn(&(compile_code_error.to_owned())) { + Ok(()) => false, + Err(e) => e.contains("Bad schema"), + }; assert_eq!( execute_fn(&(code_ok.to_owned()), &"test_fun".to_owned(), json!(10)), - true + Ok(()) ); - assert_eq!( - execute_fn( - &(execute_code_error.to_owned()), - &"test_fun".to_owned(), - json!(10) - ), - false - ); - assert_eq!(compile_fn(&(code_ok.to_owned())), true); - assert_eq!(compile_fn(&(compile_code_error.to_owned())), true); + assert_eq!(err_execute, true); + assert_eq!(compile_fn(&(code_ok.to_owned())), Ok(())); + assert_eq!(err_compile, true); } diff --git a/package-lock.json b/package-lock.json index 25bbf9e29..27b958cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,24 @@ "": { "name": "context-aware-configuration", "version": "0.0.1", + "dependencies": { + "axios": "^1.6.7", + "eslint": "^8.56.0" + }, "devDependencies": { "daisyui": "^4.3.1", "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -35,12 +47,94 @@ "node": ">=0.1.90" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@faker-js/faker": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", "dev": true }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -93,7 +187,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -106,7 +199,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -115,7 +207,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -165,11 +256,34 @@ "node": "*" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -185,7 +299,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -227,6 +340,11 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -263,8 +381,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/aws-sign2": { "version": "0.7.0", @@ -281,11 +398,20 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -335,7 +461,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -362,6 +487,14 @@ "base64-js": "^1.1.2" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -500,7 +633,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -568,8 +700,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/core-util-is": { "version": "1.0.2", @@ -577,6 +708,19 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-selector-tokenizer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", @@ -645,6 +789,22 @@ "node": ">=0.10" } }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -654,11 +814,15 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -701,6 +865,17 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -726,6 +901,215 @@ "node": ">=0.8.0" } }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -750,8 +1134,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -784,8 +1167,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastparse": { "version": "1.1.2", @@ -797,11 +1184,21 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -844,12 +1241,64 @@ "node": ">=4.0.0" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, "node_modules/flatted": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -859,11 +1308,23 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -901,7 +1362,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -921,7 +1381,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -929,6 +1388,25 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -1060,11 +1538,41 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1073,8 +1581,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1104,7 +1611,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1122,7 +1628,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1139,12 +1644,25 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1181,12 +1699,28 @@ "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", "dev": true }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1196,8 +1730,12 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -1220,6 +1758,26 @@ "verror": "1.10.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -1244,6 +1802,20 @@ "node": ">=4" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1256,6 +1828,11 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1294,7 +1871,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -1312,7 +1888,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -1330,7 +1905,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1362,6 +1936,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1391,6 +1970,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -1489,11 +2073,65 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -1503,15 +2141,30 @@ "node": ">=6" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1874,6 +2527,14 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-ms": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", @@ -1889,6 +2550,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -1899,7 +2565,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -1923,7 +2588,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -1992,21 +2656,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -2096,6 +2780,25 @@ "uuid": "bin/uuid" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2175,7 +2878,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2187,7 +2889,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -2317,6 +3018,11 @@ "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", "dev": true }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2362,6 +3068,28 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -2403,7 +3131,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -2459,6 +3186,20 @@ "extsprintf": "^1.2.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2499,8 +3240,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xmlbuilder": { "version": "15.1.1", @@ -2525,6 +3265,17 @@ "engines": { "node": ">= 14" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index a8787fd3e..dfe33efdc 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,9 @@ "daisyui": "^4.3.1", "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5" + }, + "dependencies": { + "axios": "^1.6.7", + "eslint": "^8.56.0" } } From 06974f170eed0b944484ee699de6c4a5cf3dd19b Mon Sep 17 00:00:00 2001 From: "ayush.jain@juspay.in" <ayush.jain@juspay.in> Date: Wed, 21 Feb 2024 15:04:18 +0530 Subject: [PATCH 274/352] fix: Do not remove keys with null value on merge --- Cargo.lock | 23 ------ Cargo.toml | 1 - crates/cac_client/Cargo.toml | 1 - crates/cac_client/src/eval.rs | 72 +++++++++++-------- crates/cac_client/src/lib.rs | 1 + crates/context-aware-config/Cargo.toml | 1 - .../src/api/context/handlers.rs | 2 +- 7 files changed, 43 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dce85ee3..76d4a8ee5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -615,7 +615,6 @@ dependencies = [ "actix-web", "chrono", "derive_more", - "json-patch", "jsonlogic", "log", "once_cell", @@ -860,7 +859,6 @@ dependencies = [ "futures", "futures-util", "itertools 0.10.5", - "json-patch", "jsonschema", "leptos", "leptos_actix", @@ -1926,18 +1924,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" -dependencies = [ - "serde", - "serde_json", - "thiserror", - "treediff", -] - [[package]] name = "jsonlogic" version = "0.5.1" @@ -3916,15 +3902,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "treediff" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" -dependencies = [ - "serde_json", -] - [[package]] name = "try-lock" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index f9c007fb5..e5f5afa65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ uuid = {version = "1.3.4", features = ["v4", "serde"]} reqwest = { version = "0.11.18", features = ["json"]} jsonschema = "~0.17" jsonlogic = "0.5.1" -json-patch = "1.0.0" rs-snowflake = "0.6.0" rusoto_kms = "0.48.0" rusoto_signature = "0.48.0" diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index b66d0ba7e..3d235cbc7 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -15,4 +15,3 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } -json-patch = { workspace = true } diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index c228ac0fc..c6dd3b1c2 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -5,10 +5,26 @@ use crate::{utils::core::MapError, Context}; use jsonlogic; use serde_json::{json, Map, Value}; +pub fn merge(doc: &mut Value, patch: &Value) { + if !patch.is_object() { + *doc = patch.clone(); + return; + } + + if !doc.is_object() { + *doc = Value::Object(Map::new()); + } + let map = doc.as_object_mut().unwrap(); + for (key, value) in patch.as_object().unwrap() { + merge(map.entry(key.as_str()).or_insert(Value::Null), value); + } +} + fn get_overrides( query_data: &Map<String, Value>, contexts: &Vec<Context>, overrides: &Map<String, Value>, + mut on_override_select: Option<&mut dyn FnMut(Context)>, ) -> serde_json::Result<Value> { let mut required_overrides: Value = json!({}); @@ -19,7 +35,11 @@ fn get_overrides( { for override_key in &context.override_with_keys { if let Some(overriden_value) = overrides.get(override_key) { - json_patch::merge(&mut required_overrides, overriden_value) + merge(&mut required_overrides, overriden_value); + match on_override_select { + Some(ref mut func) => func(context.clone()), + None => (), + } } } } @@ -34,7 +54,7 @@ fn merge_overrides_on_default_config( ) { overrides.into_iter().for_each(|(key, val)| { if let Some(og_val) = default_config.get_mut(&key) { - json_patch::merge(og_val, &val) + merge(og_val, &val) } else { log::error!("CAC: found non-default_config key: {key} in overrides"); } @@ -47,9 +67,11 @@ pub fn eval_cac( overrides: &Map<String, Value>, query_data: &Map<String, Value>, ) -> Result<Map<String, Value>, String> { - let overrides: Map<String, Value> = get_overrides(&query_data, &contexts, &overrides) - .and_then(|x| serde_json::from_value(x)) - .map_err_to_string()?; + let on_override_select: Option<&mut dyn FnMut(Context)> = None; + let overrides: Map<String, Value> = + get_overrides(&query_data, &contexts, &overrides, on_override_select) + .and_then(serde_json::from_value) + .map_err_to_string()?; merge_overrides_on_default_config(&mut default_config, overrides); let overriden_config = default_config; Ok(overriden_config) @@ -61,35 +83,23 @@ pub fn eval_cac_with_reasoning( overrides: &Map<String, Value>, query_data: &Map<String, Value>, ) -> Result<Map<String, Value>, String> { - let mut required_overrides: Value = json!({}); let mut reasoning: Vec<Value> = vec![]; - for context in contexts.iter() { - if let Ok(Value::Bool(true)) = - jsonlogic::apply(&context.condition, &json!(query_data)) - { - for override_key in &context.override_with_keys { - if let Some(overriden_value) = overrides.get(override_key) { - json_patch::merge(&mut required_overrides, overriden_value); - reasoning.push(json!({ - "context": context.condition, - "override": context.override_with_keys - })); - } - } - } - } - - let applied_overrides: Map<String, Value> = - serde_json::from_value(required_overrides).map_err_to_string()?; + let applied_overrides: Map<String, Value> = get_overrides( + &query_data, + &contexts, + &overrides, + Some(&mut |context| { + reasoning.push(json!({ + "context": context.condition, + "override": context.override_with_keys + })) + }), + ) + .and_then(serde_json::from_value) + .map_err_to_string()?; - applied_overrides.into_iter().for_each(|(key, val)| { - if let Some(og_val) = default_config.get_mut(&key) { - json_patch::merge(og_val, &val) - } else { - log::error!("CAC: found non-default_config key: {key} in overrides"); - } - }); + merge_overrides_on_default_config(&mut default_config, applied_overrides); let mut overriden_config = default_config; overriden_config.insert("metadata".into(), json!(reasoning)); Ok(overriden_config) diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index f0302e05e..8e727324e 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -212,3 +212,4 @@ pub static CLIENT_FACTORY: Lazy<ClientFactory> = pub use eval::eval_cac; pub use eval::eval_cac_with_reasoning; +pub use eval::merge; diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 51d7dd60e..e826155ba 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -43,7 +43,6 @@ diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } urlencoding = { workspace = true } jsonschema = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls"] } -json-patch = { workspace = true } rand = { workspace = true } service-utils = { path = "../service-utils" } experimentation-platform = { path = "../experimentation-platform" } diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index f12a4a2e2..f268db015 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -212,7 +212,7 @@ fn update_override_of_existing_ctx( .filter(dsl::id.eq(&ctx.id)) .select(dsl::override_) .first(conn)?; - json_patch::merge(&mut new_override, &ctx.override_); + cac_client::merge(&mut new_override, &ctx.override_); let new_override_id = hash(&new_override); let new_ctx = Context { override_: new_override, From 372c9de4fe2e880ff1bb1e235ae5f4fe597096c8 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 22 Feb 2024 12:05:26 +0000 Subject: [PATCH 275/352] chore(version): v0.24.0 [skip ci] --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 4 ++-- crates/cac_client/CHANGELOG.md | 6 ++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 8 ++++++++ crates/context-aware-config/Cargo.toml | 2 +- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ae395f6..1832fae89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.24.0 - 2024-02-22 +### Package updates +- cac_client bumped to cac_client-v0.5.1 +- context-aware-config bumped to context-aware-config-v0.18.0 +### Global changes +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in +#### Features +- PICAF-25876 CRUD APIs for function validator - (7c0c963) - ankit.mahato + +- - - + ## v0.23.0 - 2024-02-22 ### Package updates - context-aware-config bumped to context-aware-config-v0.17.0 diff --git a/Cargo.lock b/Cargo.lock index 76d4a8ee5..1649b4c5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.5.0" +version = "0.5.1" dependencies = [ "actix-web", "chrono", @@ -834,7 +834,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.17.0" +version = "0.18.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 66479b441..115d38fc5 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.5.1 - 2024-02-22 +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in + +- - - + ## cac_client-v0.5.0 - 2024-01-04 #### Features - working resolve page - (803dfbd) - Kartik Gajendra diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 3d235cbc7..828c7a0d4 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.5.0" +version = "0.5.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index b6411d8bb..4b53719b7 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.18.0 - 2024-02-22 +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in +#### Features +- PICAF-25876 CRUD APIs for function validator - (7c0c963) - ankit.mahato + +- - - + ## context-aware-config-v0.17.0 - 2024-02-22 #### Features - [PICAF-25877] js eval with node exec - (adc9b19) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index e826155ba..e05441c0e 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.17.0" +version = "0.18.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From f039558bd035475944bb50f3a7542b7ea3cb5f59 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 23 Feb 2024 18:47:06 +0530 Subject: [PATCH 276/352] fix: fix copy of experiment ID --- crates/context-aware-config/src/api/context/handlers.rs | 2 +- crates/frontend/src/pages/ExperimentList/utils.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index f268db015..a17bc9632 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -97,7 +97,7 @@ fn validate_dimensions_and_calculate_priority( let expected_dimension_name = dimension_condition.var; let (dimension_value_schema, _) = dimension_schema_map .get(&expected_dimension_name) - .ok_or("No matching `dimension` for in dimension table")?; + .ok_or(format!("No matching `dimension` {expected_dimension_name} in dimension table").as_str())?; validate_context_jsonschema( object_key, diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 79c0b20ad..90760a0da 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -20,12 +20,14 @@ pub fn experiment_table_columns() -> Vec<Column> { let experiment_id = row.get("id").map_or(String::from(""), |value| { value.as_str().unwrap_or("").to_string() }); - let experiment_id_copy = experiment_id.to_string(); + let experiment_id_copy = experiment_id.clone(); let handle_copy = move |event: MouseEvent| { event.prevent_default(); - let copy_code = - format!("navigator.clipboard.writeText({})", experiment_id_copy); + let copy_code = format!( + "navigator.clipboard.writeText('{}')", + &experiment_id_copy + ); match js_sys::eval(©_code) { Ok(_) => { set_copied.set(true); From f2a437b5384b2f8d6021d8ddafe120a875d0192c Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 26 Feb 2024 10:11:36 +0000 Subject: [PATCH 277/352] chore(version): v0.24.1 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1832fae89..f1fc3bcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.24.1 - 2024-02-26 +### Package updates +- context-aware-config bumped to context-aware-config-v0.18.1 +### Global changes +#### Bug Fixes +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik + +- - - + ## v0.24.0 - 2024-02-22 ### Package updates - cac_client bumped to cac_client-v0.5.1 diff --git a/Cargo.lock b/Cargo.lock index 1649b4c5e..2fbcabef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.18.0" +version = "0.18.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 4b53719b7..d8e402343 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.18.1 - 2024-02-26 +#### Bug Fixes +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik + +- - - + ## context-aware-config-v0.18.0 - 2024-02-22 #### Bug Fixes - PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index e05441c0e..b3b2f83e9 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.18.0" +version = "0.18.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 8ccb942a777d958b4e1162268e8b031018c3c658 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Tue, 27 Feb 2024 14:12:12 +0530 Subject: [PATCH 278/352] fix: returning error response if CAC call not 200 --- .../src/api/experiments/handlers.rs | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 6621236e2..81b9a9bb7 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -56,7 +56,7 @@ pub fn endpoints(scope: Scope) -> Scope { async fn process_http_response( response: Result<Response, reqwest::Error>, -) -> Result<Option<Vec<ContextPutResp>>, Error> { +) -> Result<Vec<ContextPutResp>, Error> { match response { Ok(res) if res.status().is_success() => { match res.json::<Vec<ContextBulkResponse>>().await { @@ -72,7 +72,7 @@ async fn process_http_response( } acc }); - Ok(Some(contexts)) + Ok(contexts) } Err(e) => { log::error!("Failed to parse JSON response: {}", e); @@ -222,19 +222,12 @@ async fn create( .send() .await; + // directly return an error response if not a 200 response let created_contexts = process_http_response(response).await?; - - match created_contexts { - Some(contexts) => { - for i in 0..contexts.len() { - let created_context = &contexts[i]; - variants[i].context_id = Some(created_context.context_id.clone()); - variants[i].override_id = Some(created_context.override_id.clone()); - } - } - None => { - log::info!("No contexts were created or returned."); - } + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; + variants[i].context_id = Some(created_context.context_id.clone()); + variants[i].override_id = Some(created_context.override_id.clone()); } // inserting experiment in db @@ -699,23 +692,13 @@ async fn update_overrides( .send() .await; - let created_contexts = process_http_response(response).await; - - match created_contexts { - Ok(Some(contexts)) => { - for i in 0..contexts.len() { - let created_context = &contexts[i]; + // directly return an error response if not a 200 response + let created_contexts = process_http_response(response).await?; + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; - new_variants[i].context_id = Some(created_context.context_id.clone()); - new_variants[i].override_id = Some(created_context.override_id.clone()); - } - } - Ok(None) => { - log::info!("No contexts were created or returned."); - } - Err(e) => { - log::error!("An error occurred: {}", e); - } + new_variants[i].context_id = Some(created_context.context_id.clone()); + new_variants[i].override_id = Some(created_context.override_id.clone()); } /*************************** Updating experiment in DB **************************/ From aa56d9bf94f2c7a4b3941c63befc1a4b43649c95 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Tue, 27 Feb 2024 09:52:30 +0000 Subject: [PATCH 279/352] chore(version): v0.24.2 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1fc3bcde..ba82eb868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.24.2 - 2024-02-27 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.4 +### Global changes + +- - - + ## v0.24.1 - 2024-02-26 ### Package updates - context-aware-config bumped to context-aware-config-v0.18.1 diff --git a/Cargo.lock b/Cargo.lock index 2fbcabef0..8cd17be6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.9.3" +version = "0.9.4" dependencies = [ "actix", "actix-web", diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 85c6d8625..0f342e648 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.9.4 - 2024-02-27 +#### Bug Fixes +- returning error response if CAC call not 200 - (fa0eb5e) - Shubhranshu Sanjeev + +- - - + ## experimentation-platform-v0.9.3 - 2024-02-15 #### Bug Fixes - fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 2b6bd8ec3..caa1000cf 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.9.3" +version = "0.9.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From a270deea4c12b0ab15bdaf312728de7634753983 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Mon, 26 Feb 2024 17:57:10 +0530 Subject: [PATCH 280/352] fix: add traffic percentage to experiments table --- .../src/pages/ExperimentList/utils.rs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 90760a0da..807c7eec2 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -64,23 +64,35 @@ pub fn experiment_table_columns() -> Vec<Column> { .into_view() }, ), - Column::new("status".to_string(), None, |value: &str, _| { - let badge_color = match value { - "CREATED" => "badge-info", - "INPROGRESS" => "badge-warning", - "CONCLUDED" => "badge-success", - &_ => "info", - }; - let class = format!("badge {}", badge_color); - view! { + Column::new( + "status".to_string(), + None, + |value: &str, row: &Map<String, Value>| { + let badge_color = match value { + "CREATED" => "badge-info", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-success", + &_ => "info", + }; + let class = format!("badge {}", badge_color); + let traffic_percentage = row.get("traffic_percentage"); + let traffic_percentage = traffic_percentage.map(|val| val.as_u64().unwrap_or(0)).unwrap_or(0); + view! { <div class={class}> <span class="text-white font-semibold text-xs"> - {value.to_string()} + { + if value == "INPROGRESS" { + format!("{}: {}%", value.to_string(), traffic_percentage) + } else { + value.to_string() + } + } </span> </div> } .into_view() - }), + }, + ), Column::new( "context".to_string(), None, From f3bfcc0937114834c96bf7d65cd26616ca8041bf Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Mon, 26 Feb 2024 17:57:48 +0530 Subject: [PATCH 281/352] fix: transpose columns in single experiment page for variants --- crates/context-aware-config/src/main.rs | 4 +- .../src/components/experiment/experiment.rs | 10 +--- .../src/components/experiment/utils.rs | 59 +++++++++++-------- .../experiment_form/experiment_form.rs | 2 +- crates/frontend/src/pages/Experiment/mod.rs | 2 +- .../src/pages/ExperimentList/utils.rs | 4 +- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index 083767227..b1e1142d8 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -155,7 +155,7 @@ async fn main() -> Result<()> { let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); // Generate the list of routes in your Leptos App let routes = generate_route_list(move || { - return view! { <App app_envs={routes_ui_envs.clone()} /> }; + return view! { <App app_envs=routes_ui_envs.clone()/> }; }); HttpServer::new(move || { @@ -208,7 +208,7 @@ async fn main() -> Result<()> { .leptos_routes( leptos_options.to_owned(), routes.to_owned(), - move || view! { <App app_envs={leptos_envs.clone()} /> }, + move || view! { <App app_envs=leptos_envs.clone()/> }, ) .service( scope(&base) diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index 2cd703fd2..79d44f0e6 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -4,9 +4,8 @@ use leptos::*; use crate::components::condition_pills::utils::extract_and_format; use crate::components::table::table::Table; -use crate::components::table::types::Column; -use super::utils::gen_variant_rows; +use super::utils::gen_variant_table; use crate::types::{Experiment, ExperimentStatusType}; #[component] @@ -191,12 +190,7 @@ where let experiment_clone = experiment_rc.clone(); move || { let exp = experiment_clone.clone(); - let rows = gen_variant_rows(&exp.variants).unwrap(); - let mut columns: Vec<Column> = Vec::new(); - columns.push(Column::default("Variant".into())); - for okey in exp.override_keys.as_array().unwrap().into_iter() { - columns.push(Column::default(okey.as_str().unwrap().into())); - } + let (rows, columns) = gen_variant_table(&exp.variants).unwrap(); view! { <Table table_style="abc".to_string() diff --git a/crates/frontend/src/components/experiment/utils.rs b/crates/frontend/src/components/experiment/utils.rs index 028886c17..47d3d0915 100644 --- a/crates/frontend/src/components/experiment/utils.rs +++ b/crates/frontend/src/components/experiment/utils.rs @@ -1,26 +1,39 @@ -use crate::types::{Variant, VariantType}; +use std::collections::HashMap; + +use crate::{ + components::table::types::Column, + types::{Variant, VariantType}, +}; +use leptos::{view, IntoView}; use serde_json::{Map, Value}; -pub fn gen_variant_rows(variants: &[Variant]) -> Result<Vec<Map<String, Value>>, String> { - let rows = variants - .iter() - .enumerate() - .map(|(i, variant)| { - let variant_name = match variant.variant_type { - VariantType::CONTROL => "Control".into(), - VariantType::EXPERIMENTAL => format!("Variant-{i}"), - }; - let mut row_data = variant - .overrides - .iter() - .map(|(key, value)| (key.clone(), value.clone())) - .collect::<Vec<(String, Value)>>(); - row_data.extend_from_slice(&[ - (String::from("Variant"), variant_name.into()), - (String::from("variant_id"), variant.id.clone().into()), - ]); - Map::from_iter(row_data) - }) - .collect::<Vec<Map<String, Value>>>(); - Ok(rows) +pub fn gen_variant_table( + variants: &[Variant], +) -> Result<(Vec<Map<String, Value>>, Vec<Column>), String> { + let mut columns = vec![Column::default("Config Key".into())]; + let mut row_map: HashMap<&String, Map<String, Value>> = HashMap::new(); + for (i, variant) in variants.iter().enumerate() { + let name = match variant.variant_type { + VariantType::CONTROL => format!("{}", variant.variant_type), + VariantType::EXPERIMENTAL => format!("Variant-{}", i), + }; + columns.push(Column::new(name.clone(), None, |value: &str, _| { + view! { <span>{value.to_string()}</span> }.into_view() + })); + for (config, value) in variant.overrides.iter() { + match row_map.get_mut(config) { + Some(c) => { + c.insert(name.clone(), value.clone()); + } + None => { + let mut m = Map::new(); + m.insert("Config Key".into(), Value::String(config.clone())); + m.insert(name.clone(), value.clone()); + row_map.insert(config, m); + } + } + } + } + let rows = row_map.into_values().collect(); + Ok((rows, columns)) } diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index 004132cc4..d4afd1986 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -145,7 +145,7 @@ where <div class="my-4"> <ContextForm - dimensions={dimensions.clone()} + dimensions=dimensions.clone() context=context handle_change=handle_context_form_change is_standalone=false diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs index 00adf2fcb..db396ebb8 100644 --- a/crates/frontend/src/pages/Experiment/mod.rs +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -57,7 +57,7 @@ pub fn experiment_page() -> impl IntoView { let handle_start = move |experiment_id: String| { spawn_local(async move { let tenant = tenant_rs.get(); - let _ = ramp_experiment(&experiment_id, 1, &tenant).await; + let _ = ramp_experiment(&experiment_id, 0, &tenant).await; combined_resource.refetch(); }) }; diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/ExperimentList/utils.rs index 807c7eec2..e9a70f22d 100644 --- a/crates/frontend/src/pages/ExperimentList/utils.rs +++ b/crates/frontend/src/pages/ExperimentList/utils.rs @@ -76,7 +76,9 @@ pub fn experiment_table_columns() -> Vec<Column> { }; let class = format!("badge {}", badge_color); let traffic_percentage = row.get("traffic_percentage"); - let traffic_percentage = traffic_percentage.map(|val| val.as_u64().unwrap_or(0)).unwrap_or(0); + let traffic_percentage = traffic_percentage + .map(|val| val.as_u64().unwrap_or(0)) + .unwrap_or(0); view! { <div class={class}> <span class="text-white font-semibold text-xs"> From fb5bb4685222b55015de32591e0c183582b5140b Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Tue, 27 Feb 2024 15:50:59 +0530 Subject: [PATCH 282/352] feat: autodeploy --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index ac5fc2463..77de2df20 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -238,7 +238,7 @@ pipeline { steps { sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_SBX}/release' \ --header 'Content-Type: application/json' \ - --header 'Authorization: Basic ${CREDS}' \ + --header 'x-api-key: ${CREDS}' \ --data-raw '{ "service": ["CONTEXT_AWARE_CONFIG"], "release_manager": "jenkins.jenkins", From 6667206df5f01cf7898e80d6778b09879aaa86d2 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 28 Feb 2024 12:16:08 +0000 Subject: [PATCH 283/352] chore(version): v0.25.0 [skip ci] --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba82eb868..9d9a96875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.25.0 - 2024-02-28 +### Package updates +- context-aware-config bumped to context-aware-config-v0.18.2 +### Global changes +#### Bug Fixes +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik +- [PICAF-26196] add traffic percentage to experiments table - (5fb0221) - Kartik +#### Features +- autodeploy - (94bc7c7) - Kartik + +- - - + ## v0.24.2 - 2024-02-27 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.9.4 diff --git a/Cargo.lock b/Cargo.lock index 8cd17be6d..83bc8ae8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.18.1" +version = "0.18.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index d8e402343..c4b8720ac 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.18.2 - 2024-02-28 +#### Bug Fixes +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik + +- - - + ## context-aware-config-v0.18.1 - 2024-02-26 #### Bug Fixes - [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index b3b2f83e9..11eb9230e 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.18.1" +version = "0.18.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From a5e50d6a144593998e81c3f0b93ae2594f04be16 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Wed, 28 Feb 2024 18:06:47 +0530 Subject: [PATCH 284/352] fix: autodeploy --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 77de2df20..1c84a2815 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -230,7 +230,7 @@ pipeline { branch 'main' } environment { - CREDS = credentials('AUTOPILOT_AUTH_HEADER') + CREDS = credentials('SDK_SBX_AP_KEY') COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") From 39102993d15c0fa5a51fdd8998898ad64867f00e Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Wed, 14 Feb 2024 15:31:58 +0530 Subject: [PATCH 285/352] feat: added test,publish api for functions --- Dockerfile | 2 +- .../src/api/config/handlers.rs | 1 - .../src/api/functions/handlers.rs | 124 ++- .../src/api/functions/types.rs | 13 + crates/context-aware-config/src/auth.rs | 14 + .../src/validation_functions.rs | 44 +- makefile | 3 +- package-lock.json | 831 ++---------------- package.json | 5 +- 9 files changed, 223 insertions(+), 814 deletions(-) diff --git a/Dockerfile b/Dockerfile index 21e446717..9adc3a02b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh RUN npm ci --loglevel=info RUN cd crates/context-aware-config/ && npm ci RUN mkdir -p target/node_modules -RUN cp -a crates/context-aware-config/node_modules target/node_modules/ +RUN cp -a crates/context-aware-config/node_modules target/ # building frontend RUN --mount=type=ssh cd crates/frontend \ diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index e76d79b4f..da8411fc6 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -129,7 +129,6 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR if let Ok(true) = is_not_modified { return Ok(HttpResponse::NotModified().finish()); } - let res = HttpResponse::Ok().json(generate_cac(&mut conn).await?); add_last_modified_header(max_created_at, res) diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index 2bec7e6a4..cb153141f 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -4,26 +4,28 @@ use base64::prelude::*; use super::helpers::{decode_function, fetch_function}; use crate::{ + api::functions::types::{Stage, TestParam}, db::{ self, models::Function, - schema::functions::{dsl::functions, function_name}, + schema::functions::{dsl, dsl::functions, function_name}, }, validation_functions, }; use actix_web::{ delete, error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, - get, patch, post, - web::{self, Json}, + get, patch, post, put, + web::{self, Json, Path}, HttpResponse, Result, Scope, }; use chrono::Utc; use dashboard_auth::types::User; use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; -use serde_json::json; +use serde_json::{json, Value}; use service_utils::service::types::DbConnection; -use validation_functions::compile_fn; +use tracing_utils::tracing; +use validation_functions::{compile_fn, execute_fn}; use super::types::{CreateFunctionRequest, UpdateFunctionRequest}; @@ -34,6 +36,8 @@ pub fn endpoints() -> Scope { .service(get) .service(list_functions) .service(delete_function) + .service(test) + .service(publish) } #[post("")] @@ -45,7 +49,7 @@ async fn create( let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - if let Err(e) = compile_fn(&req.function.to_string()) { + if let Err(e) = compile_fn(&req.function) { return Err(ErrorBadRequest(json!({ "message": e }))); } @@ -73,7 +77,7 @@ async fn create( } Err(e) => match e { diesel::result::Error::DatabaseError(kind, e) => { - log::info!("Function error: {:?}", e); + tracing::error!("Function error: {:?}", e); match kind { diesel::result::DatabaseErrorKind::UniqueViolation => { return Err(ErrorBadRequest( @@ -88,7 +92,7 @@ async fn create( } } _ => { - log::info!("Function creation failed with error: {e}"); + tracing::error!("Function creation failed with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "An error occured please contact the admin."}), )); @@ -111,11 +115,11 @@ async fn update( let result = match fetch_function(&f_name, &mut conn) { Ok(val) => val, Err(diesel::result::Error::NotFound) => { - log::info!("Function not found."); + tracing::error!("Function not found."); return Err(ErrorBadRequest(json!({"message": "Function not found."}))); } Err(e) => { - log::info!("Failed to update Function with error: {e}"); + tracing::error!("Failed to update Function with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "Failed to update Function"}), )); @@ -158,7 +162,7 @@ async fn update( Ok(Json(res)) } Err(e) => { - log::info!("Function updation failed with error: {e}"); + tracing::error!("Function updation failed with error: {e}"); Err(ErrorInternalServerError( json!({"message": "Failed to update Function"}), )) @@ -178,7 +182,7 @@ async fn get(params: web::Path<String>, db_conn: DbConnection) -> Result<HttpRes Ok(HttpResponse::Ok().json(function)) } Err(e) => { - log::info!("Error getting function: {e}"); + tracing::error!("Error getting function: {e}"); Err(ErrorInternalServerError( json!({"message": "Function does not exists."}), )) @@ -200,7 +204,7 @@ async fn list_functions(db_conn: DbConnection) -> Result<Json<Vec<Function>>> { Ok(Json(function_list)) } Err(e) => { - log::info!("Error getting the functions: {e}"); + tracing::error!("Error getting the functions: {e}"); Err(ErrorInternalServerError( json!({"message": "Error getting the functions."}), )) @@ -222,12 +226,102 @@ async fn delete_function( match deleted_row { Ok(0) => Err(ErrorNotFound(json!({"message": "Function not found."}))), Ok(_) => { - log::info!("{f_name} function deleted by {}", user.email); + tracing::info!("{f_name} function deleted by {}", user.email); Ok(HttpResponse::NoContent().finish()) } Err(e) => { - log::error!("function delete query failed with error: {e}"); + tracing::error!("function delete query failed with error: {e}"); Err(ErrorInternalServerError("")) } } } + +#[put("/{function_name}/{stage}/test")] +async fn test( + params: Path<TestParam>, + request: web::Json<Value>, + db_conn: DbConnection, +) -> actix_web::Result<HttpResponse> { + let DbConnection(mut conn) = db_conn; + let path_params = params.into_inner(); + let fun_name = &path_params.function_name; + let req = request.into_inner(); + let mut function = match fetch_function(fun_name, &mut conn) { + Ok(val) => val, + Err(diesel::result::Error::NotFound) => { + tracing::error!("Function not found."); + return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + } + Err(e) => { + tracing::error!("Failed to update Function with error: {e}"); + return Err(ErrorInternalServerError( + json!({"message": "Failed to update Function due to unexpected DB issue"}), + )); + } + }; + decode_function(&mut function)?; + let result = match path_params.stage { + Stage::DRAFT => execute_fn(&function.draft_code, fun_name, req), + Stage::PUBLISHED => match function.published_code { + Some(code) => execute_fn(&code, fun_name, req), + None => { + tracing::error!("Function test failed: function not published yet"); + Err("Function test failed as function not published yet".to_owned()) + } + }, + }; + + match result { + Ok(()) => Ok(HttpResponse::Ok() + .json(json!({"message": "Function validated the given value successfully"}))), + Err(e) => Err(ErrorBadRequest(json!({ "message": e }))), + } +} + +#[put("/{function_name}/publish")] +async fn publish( + user: User, + params: web::Path<String>, + db_conn: DbConnection, +) -> actix_web::Result<HttpResponse> { + let DbConnection(mut conn) = db_conn; + let fun_name = params.into_inner(); + + let function = match fetch_function(&fun_name, &mut conn) { + Ok(val) => val, + Err(diesel::result::Error::NotFound) => { + tracing::error!("Function not found."); + return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + } + Err(e) => { + tracing::error!("Failed to update Function with error: {e}"); + return Err(ErrorInternalServerError( + json!({"message": "Failed to update Function"}), + )); + } + }; + + let updated_function: Result<Function, diesel::result::Error> = + diesel::update(functions) + .filter(dsl::function_name.eq(fun_name)) + .set(( + dsl::published_code.eq(Some(function.draft_code.clone())), + dsl::published_runtime_version + .eq(Some(function.draft_runtime_version.clone())), + dsl::published_by.eq(Some(user.email)), + dsl::published_at.eq(Some(Utc::now().naive_utc())), + )) + .get_result(&mut conn); + + match updated_function { + Ok(_) => Ok(HttpResponse::Ok().json(json!({ + "message": "Function published successfully." + }))), + Err(e) => { + tracing::error!("Function publish failed with error: {e}"); + Err(ErrorInternalServerError( + json!({"message": "Failed to publish Function due to unexpected DB issue"}), + )) + } + } +} diff --git a/crates/context-aware-config/src/api/functions/types.rs b/crates/context-aware-config/src/api/functions/types.rs index 10f779f4e..246effd0c 100644 --- a/crates/context-aware-config/src/api/functions/types.rs +++ b/crates/context-aware-config/src/api/functions/types.rs @@ -25,3 +25,16 @@ pub struct FunctionResponse { pub published_at: String, pub drafted_at: String, } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] +pub enum Stage { + DRAFT, + PUBLISHED, +} + +#[derive(Deserialize)] +pub struct TestParam { + pub function_name: String, + pub stage: Stage, +} diff --git a/crates/context-aware-config/src/auth.rs b/crates/context-aware-config/src/auth.rs index f3c0f9d67..e2b2a826f 100644 --- a/crates/context-aware-config/src/auth.rs +++ b/crates/context-aware-config/src/auth.rs @@ -174,6 +174,20 @@ pub mod functions { user_permissions: ("manager".into(), "RW".into()), }, ), + ( + "PUT::/function/{function_name}/{stage}/test", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::/function/{function_name}/publish", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), ]) } } diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 1f5c78855..c539c42b4 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -1,11 +1,12 @@ use serde_json::{json, Value}; use std::process::Command; use std::str; +use tracing_utils::tracing; const IMPORT_CODE: &str = r#" /*eslint no-unused-vars: "off"*/ /*eslint no-extra-semi: "off"*/ - const axios = require("/target/node_modules/axios"); + const axios = require("./target/node_modules/axios"); "#; const EXIT_LOGIC_CODE: &str = r#" @@ -15,29 +16,33 @@ const EXIT_LOGIC_CODE: &str = r#" "#; const ES_LINT_CODE: &str = r#" - const eslint = require("eslint"); - const linter = new eslint.ESLint({ - useEslintrc: false, - overrideConfig: { - extends: ["eslint:recommended"], - parserOptions: { - sourceType: "module", - ecmaVersion: "latest", - }, - env: { - browser: true, - commonjs: true, - } + const {ESLint} = require("./target/node_modules/eslint"); + const linter = new ESLint({ + useEslintrc: false, + overrideConfig: { + extends: ["eslint:recommended"], + parserOptions: { + sourceType: "module", + ecmaVersion: "latest", }, - }); + env: { + browser: true, + commonjs: true, + } + }, + }); linter.lintText(codeToLint).then((results) => { var err_count = 0; + var err_msgs = []; for (var err_obj of results) { + console.log(err_obj.messages); err_count = err_count + err_obj.errorCount; + err_msgs.push(err_obj.messages); } if (err_count > 0) { + console.error(err_msgs); process.exit(1); } }).catch((error) => { @@ -57,7 +62,7 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St .arg("-e") .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) .output(); - log::trace!("{}", format!("validation function output : {:?}", output)); + tracing::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { if !(val.status.success()) { @@ -74,7 +79,7 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St } } Err(e) => { - log::error!("js_eval error: {}", e); + tracing::error!("js_eval error: {}", e); Err(format!("js_eval error: {}", e)) } } @@ -91,20 +96,21 @@ pub fn compile_fn(code_str: &str) -> Result<(), String> { .arg("-e") .arg(eslint_logic(code_str)) .output(); + tracing::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { if !(val.status.success()) { let stderr = str::from_utf8(&val.stderr) .unwrap_or("[Invalid UTF-8 in stderr]") .to_owned(); - log::error!("{}", format!("eslint check output error: {:?}", stderr)); + tracing::error!("{}", format!("eslint check output error: {:?}", stderr)); Err(stderr) } else { Ok(()) } } Err(e) => { - log::error!("eslint check error: {}", e); + tracing::error!("eslint check error: {}", e); Err(format!("js_eval error: {}", e)) } } diff --git a/makefile b/makefile index b4811169a..9c63f037c 100644 --- a/makefile +++ b/makefile @@ -120,8 +120,7 @@ frontend: backend: -rm -rf target/node_modules npm --prefix ./crates/context-aware-config/ ci - mkdir target/node_modules - mv crates/context-aware-config/node_modules target/node_modules + mv crates/context-aware-config/node_modules target/ cargo build --color always build: frontend backend diff --git a/package-lock.json b/package-lock.json index 27b958cf5..b6d2e5d48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,24 +7,13 @@ "": { "name": "context-aware-configuration", "version": "0.0.1", - "dependencies": { - "axios": "^1.6.7", - "eslint": "^8.56.0" - }, "devDependencies": { + "axios": "^0.16.1", "daisyui": "^4.3.1", "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -47,94 +36,12 @@ "node": ">=0.1.90" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@faker-js/faker": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", "dev": true }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -187,6 +94,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -199,6 +107,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -207,6 +116,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -256,34 +166,11 @@ "node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -299,6 +186,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -340,11 +228,6 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -381,7 +264,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/aws-sign2": { "version": "0.7.0", @@ -399,19 +283,21 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha512-IMYFDrcVbUksQhsMYtWCM6KdNaDpr1NY56dpzaIgj92ecPVI29bf2sOgAf8aGTiq8UoixJD61Pj0Ahej5DPv7w==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dev": true, "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.2.3", + "is-buffer": "^1.1.5" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -461,6 +347,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -487,14 +374,6 @@ "base64-js": "^1.1.2" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -633,6 +512,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -700,7 +580,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/core-util-is": { "version": "1.0.2", @@ -708,19 +589,6 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-selector-tokenizer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", @@ -789,22 +657,6 @@ "node": ">=0.10" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -814,15 +666,11 @@ "node": ">=4.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -865,17 +713,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -901,215 +738,6 @@ "node": ">=0.8.0" } }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -1134,7 +762,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.3.2", @@ -1167,12 +796,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fastparse": { "version": "1.1.2", @@ -1184,21 +809,11 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -1241,39 +856,6 @@ "node": ">=4.0.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" - }, "node_modules/flatted": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", @@ -1284,6 +866,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, "funding": [ { "type": "individual", @@ -1308,23 +891,11 @@ "node": "*" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -1362,6 +933,7 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1381,6 +953,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -1388,25 +961,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -1538,41 +1092,11 @@ "node": ">=0.10.0" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1581,7 +1105,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1595,6 +1120,12 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -1611,6 +1142,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1628,6 +1160,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1644,25 +1177,12 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1699,28 +1219,12 @@ "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", "dev": true }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1730,12 +1234,8 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -1758,26 +1258,6 @@ "verror": "1.10.0" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -1802,20 +1282,6 @@ "node": ">=4" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1828,11 +1294,6 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1871,6 +1332,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -1888,6 +1350,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -1905,6 +1368,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1936,11 +1400,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1970,11 +1429,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -2073,65 +1527,11 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -2141,30 +1541,15 @@ "node": ">=6" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2527,14 +1912,6 @@ "node": ">=10" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/pretty-ms": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", @@ -2550,11 +1927,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -2565,6 +1937,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -2588,6 +1961,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -2656,41 +2030,21 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -2780,25 +2134,6 @@ "uuid": "bin/uuid" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2878,6 +2213,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2889,6 +2225,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -3018,11 +2355,6 @@ "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", "dev": true }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3068,28 +2400,6 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -3131,6 +2441,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -3186,20 +2497,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3240,7 +2537,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/xmlbuilder": { "version": "15.1.1", @@ -3265,17 +2563,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index dfe33efdc..24a14886c 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,9 @@ "load_cac_tests": "npx newman dir-import postman/cac -o postman/cac.postman_collection.json" }, "devDependencies": { + "axios": "^0.16.1", "daisyui": "^4.3.1", "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5" - }, - "dependencies": { - "axios": "^1.6.7", - "eslint": "^8.56.0" } } From e6d8ed2bc1a533cae15f8c542d5b8a09f1a63381 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 29 Feb 2024 05:53:48 +0000 Subject: [PATCH 286/352] chore(version): v0.26.0 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9a96875..31c8c2830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.26.0 - 2024-02-29 +### Package updates +- context-aware-config bumped to context-aware-config-v0.19.0 +### Global changes +#### Bug Fixes +- autodeploy - (43a4ada) - Kartik +#### Features +- [PICAF-25879] added test,publish api for functions - (050ab24) - Pratik Mishra + +- - - + ## v0.25.0 - 2024-02-28 ### Package updates - context-aware-config bumped to context-aware-config-v0.18.2 diff --git a/Cargo.lock b/Cargo.lock index 83bc8ae8b..f194cc978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.18.2" +version = "0.19.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index c4b8720ac..d5e33369d 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.19.0 - 2024-02-29 +#### Features +- [PICAF-25879] added test,publish api for functions - (050ab24) - Pratik Mishra + +- - - + ## context-aware-config-v0.18.2 - 2024-02-28 #### Bug Fixes - [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 11eb9230e..3cadf9acc 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.18.2" +version = "0.19.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 5b51c638bf72a4980e4c6c9f813501fdd8b3fc03 Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Fri, 1 Mar 2024 14:11:24 +0530 Subject: [PATCH 287/352] feat: add node to app directory --- Dockerfile | 12 ++++++- Jenkinsfile | 3 +- .../src/api/functions/handlers.rs | 31 +++++++++---------- .../src/validation_functions.rs | 11 +++---- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9adc3a02b..9c542dabd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,11 +38,21 @@ RUN --mount=type=ssh cargo build --release FROM debian:bookworm-slim WORKDIR /app +ENV NODE_VERSION=18.19.0 ARG SOURCE_COMMIT ARG CONTEXT_AWARE_CONFIG_VERSION -RUN apt-get update && apt-get install -y libpq5 ca-certificates +RUN apt-get update && apt-get install -y libpq5 ca-certificates curl +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +ENV NVM_DIR=/root/.nvm +RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} +ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" +RUN node --version + + COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config COPY --from=builder /build/Cargo.toml /app/Cargo.toml COPY --from=builder /build/target/site /app/target/site diff --git a/Jenkinsfile b/Jenkinsfile index 1c84a2815..6d350d14c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -234,6 +234,7 @@ pipeline { COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") + AP_DEPLOY_VERSION = env.NEW_SEMANTIC_VERSION.replace(".", "x") } steps { sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_SBX}/release' \ @@ -243,7 +244,7 @@ pipeline { "service": ["CONTEXT_AWARE_CONFIG"], "release_manager": "jenkins.jenkins", "release_tag": "", - "new_version": "${NEW_SEMANTIC_VERSION}", + "new_version": "${AP_DEPLOY_VERSION}", "docker_image" : "${NEW_SEMANTIC_VERSION}", "priority" : 0, "cluster" : "EKS_MUM", diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index cb153141f..4dfc4500d 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -24,7 +24,6 @@ use dashboard_auth::types::User; use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{json, Value}; use service_utils::service::types::DbConnection; -use tracing_utils::tracing; use validation_functions::{compile_fn, execute_fn}; use super::types::{CreateFunctionRequest, UpdateFunctionRequest}; @@ -77,7 +76,7 @@ async fn create( } Err(e) => match e { diesel::result::Error::DatabaseError(kind, e) => { - tracing::error!("Function error: {:?}", e); + log::error!("Function error: {:?}", e); match kind { diesel::result::DatabaseErrorKind::UniqueViolation => { return Err(ErrorBadRequest( @@ -92,7 +91,7 @@ async fn create( } } _ => { - tracing::error!("Function creation failed with error: {e}"); + log::error!("Function creation failed with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "An error occured please contact the admin."}), )); @@ -115,11 +114,11 @@ async fn update( let result = match fetch_function(&f_name, &mut conn) { Ok(val) => val, Err(diesel::result::Error::NotFound) => { - tracing::error!("Function not found."); + log::error!("Function not found."); return Err(ErrorBadRequest(json!({"message": "Function not found."}))); } Err(e) => { - tracing::error!("Failed to update Function with error: {e}"); + log::error!("Failed to update Function with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "Failed to update Function"}), )); @@ -162,7 +161,7 @@ async fn update( Ok(Json(res)) } Err(e) => { - tracing::error!("Function updation failed with error: {e}"); + log::error!("Function updation failed with error: {e}"); Err(ErrorInternalServerError( json!({"message": "Failed to update Function"}), )) @@ -182,7 +181,7 @@ async fn get(params: web::Path<String>, db_conn: DbConnection) -> Result<HttpRes Ok(HttpResponse::Ok().json(function)) } Err(e) => { - tracing::error!("Error getting function: {e}"); + log::error!("Error getting function: {e}"); Err(ErrorInternalServerError( json!({"message": "Function does not exists."}), )) @@ -204,7 +203,7 @@ async fn list_functions(db_conn: DbConnection) -> Result<Json<Vec<Function>>> { Ok(Json(function_list)) } Err(e) => { - tracing::error!("Error getting the functions: {e}"); + log::error!("Error getting the functions: {e}"); Err(ErrorInternalServerError( json!({"message": "Error getting the functions."}), )) @@ -226,11 +225,11 @@ async fn delete_function( match deleted_row { Ok(0) => Err(ErrorNotFound(json!({"message": "Function not found."}))), Ok(_) => { - tracing::info!("{f_name} function deleted by {}", user.email); + log::info!("{f_name} function deleted by {}", user.email); Ok(HttpResponse::NoContent().finish()) } Err(e) => { - tracing::error!("function delete query failed with error: {e}"); + log::error!("function delete query failed with error: {e}"); Err(ErrorInternalServerError("")) } } @@ -249,11 +248,11 @@ async fn test( let mut function = match fetch_function(fun_name, &mut conn) { Ok(val) => val, Err(diesel::result::Error::NotFound) => { - tracing::error!("Function not found."); + log::error!("Function not found."); return Err(ErrorBadRequest(json!({"message": "Function not found."}))); } Err(e) => { - tracing::error!("Failed to update Function with error: {e}"); + log::error!("Failed to update Function with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "Failed to update Function due to unexpected DB issue"}), )); @@ -265,7 +264,7 @@ async fn test( Stage::PUBLISHED => match function.published_code { Some(code) => execute_fn(&code, fun_name, req), None => { - tracing::error!("Function test failed: function not published yet"); + log::error!("Function test failed: function not published yet"); Err("Function test failed as function not published yet".to_owned()) } }, @@ -290,11 +289,11 @@ async fn publish( let function = match fetch_function(&fun_name, &mut conn) { Ok(val) => val, Err(diesel::result::Error::NotFound) => { - tracing::error!("Function not found."); + log::error!("Function not found."); return Err(ErrorBadRequest(json!({"message": "Function not found."}))); } Err(e) => { - tracing::error!("Failed to update Function with error: {e}"); + log::error!("Failed to update Function with error: {e}"); return Err(ErrorInternalServerError( json!({"message": "Failed to update Function"}), )); @@ -318,7 +317,7 @@ async fn publish( "message": "Function published successfully." }))), Err(e) => { - tracing::error!("Function publish failed with error: {e}"); + log::error!("Function publish failed with error: {e}"); Err(ErrorInternalServerError( json!({"message": "Failed to publish Function due to unexpected DB issue"}), )) diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index c539c42b4..9e53ba111 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -1,7 +1,6 @@ use serde_json::{json, Value}; use std::process::Command; use std::str; -use tracing_utils::tracing; const IMPORT_CODE: &str = r#" /*eslint no-unused-vars: "off"*/ @@ -62,7 +61,7 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St .arg("-e") .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) .output(); - tracing::trace!("{}", format!("validation function output : {:?}", output)); + log::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { if !(val.status.success()) { @@ -79,7 +78,7 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St } } Err(e) => { - tracing::error!("js_eval error: {}", e); + log::error!("js_eval error: {}", e); Err(format!("js_eval error: {}", e)) } } @@ -96,21 +95,21 @@ pub fn compile_fn(code_str: &str) -> Result<(), String> { .arg("-e") .arg(eslint_logic(code_str)) .output(); - tracing::trace!("{}", format!("validation function output : {:?}", output)); + log::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { if !(val.status.success()) { let stderr = str::from_utf8(&val.stderr) .unwrap_or("[Invalid UTF-8 in stderr]") .to_owned(); - tracing::error!("{}", format!("eslint check output error: {:?}", stderr)); + log::error!("{}", format!("eslint check output error: {:?}", stderr)); Err(stderr) } else { Ok(()) } } Err(e) => { - tracing::error!("eslint check error: {}", e); + log::error!("eslint check error: {}", e); Err(format!("js_eval error: {}", e)) } } From c678efe2d0cf8d9e73eb94059948083f8b6b9b31 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 4 Mar 2024 07:44:10 +0000 Subject: [PATCH 288/352] chore(version): v0.27.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c8c2830..df44277cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.27.0 - 2024-03-04 +### Package updates +- context-aware-config bumped to context-aware-config-v0.20.0 +### Global changes +#### Features +- [PICAF-25877 add node to app directory - (9671875) - Pratik Mishra + +- - - + ## v0.26.0 - 2024-02-29 ### Package updates - context-aware-config bumped to context-aware-config-v0.19.0 diff --git a/Cargo.lock b/Cargo.lock index f194cc978..171df3032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.19.0" +version = "0.20.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index d5e33369d..8c6275253 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.20.0 - 2024-03-04 +#### Features +- [PICAF-25877 add node to app directory - (9671875) - Pratik Mishra + +- - - + ## context-aware-config-v0.19.0 - 2024-02-29 #### Features - [PICAF-25879] added test,publish api for functions - (050ab24) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 3cadf9acc..411a6473b 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.19.0" +version = "0.20.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 032dc3af0847e6c5810cc35dc9656e5230502195 Mon Sep 17 00:00:00 2001 From: "ayush.jain@juspay.in" <ayush.jain@juspay.in> Date: Thu, 22 Feb 2024 23:52:18 +0530 Subject: [PATCH 289/352] feat: Replace merge-strategy option for resolve/eval --- Cargo.lock | 2 + crates/cac_client/Cargo.toml | 2 + crates/cac_client/src/eval.rs | 73 ++++++++++++++++--- crates/cac_client/src/lib.rs | 18 ++++- .../src/api/config/handlers.rs | 13 +++- 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 171df3032..4abd7ba82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "strum", + "strum_macros", ] [[package]] diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 828c7a0d4..dee898796 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -15,3 +15,5 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs index c6dd3b1c2..fe3795dba 100644 --- a/crates/cac_client/src/eval.rs +++ b/crates/cac_client/src/eval.rs @@ -1,7 +1,7 @@ //NOTE this code is copied over from sdk-config-server with small changes for compatiblity //TODO refactor, make eval MJOS agnostic -use crate::{utils::core::MapError, Context}; +use crate::{utils::core::MapError, Context, MergeStrategy}; use jsonlogic; use serde_json::{json, Map, Value}; @@ -20,13 +20,37 @@ pub fn merge(doc: &mut Value, patch: &Value) { } } +fn replace_top_level( + doc: &mut Map<String, Value>, + patch: &Value, + mut on_override: impl FnMut(), + override_key: &String, +) { + match patch.as_object() { + Some(patch_map) => { + for (key, value) in patch_map { + doc.insert(key.clone(), value.clone()); + } + on_override(); + } + None => { + log::error!("CAC: found non-object override key: {override_key} in overrides") + } + } +} + fn get_overrides( query_data: &Map<String, Value>, contexts: &Vec<Context>, overrides: &Map<String, Value>, + merge_strategy: &MergeStrategy, mut on_override_select: Option<&mut dyn FnMut(Context)>, ) -> serde_json::Result<Value> { let mut required_overrides: Value = json!({}); + let mut on_override_select = |context: Context| match on_override_select { + Some(ref mut func) => func(context), + None => (), + }; for context in contexts.iter() { // TODO :: Add semantic version comparator in Lib @@ -35,10 +59,17 @@ fn get_overrides( { for override_key in &context.override_with_keys { if let Some(overriden_value) = overrides.get(override_key) { - merge(&mut required_overrides, overriden_value); - match on_override_select { - Some(ref mut func) => func(context.clone()), - None => (), + match merge_strategy { + MergeStrategy::REPLACE => replace_top_level( + &mut required_overrides.as_object_mut().unwrap(), + overriden_value, + || on_override_select(context.clone()), + override_key, + ), + MergeStrategy::MERGE => { + merge(&mut required_overrides, overriden_value); + on_override_select(context.clone()) + } } } } @@ -51,10 +82,16 @@ fn get_overrides( fn merge_overrides_on_default_config( default_config: &mut Map<String, Value>, overrides: Map<String, Value>, + merge_strategy: &MergeStrategy, ) { overrides.into_iter().for_each(|(key, val)| { if let Some(og_val) = default_config.get_mut(&key) { - merge(og_val, &val) + match merge_strategy { + MergeStrategy::REPLACE => { + let _ = default_config.insert(key.clone(), val.clone()); + } + MergeStrategy::MERGE => merge(og_val, &val), + } } else { log::error!("CAC: found non-default_config key: {key} in overrides"); } @@ -66,13 +103,19 @@ pub fn eval_cac( contexts: &Vec<Context>, overrides: &Map<String, Value>, query_data: &Map<String, Value>, + merge_strategy: MergeStrategy, ) -> Result<Map<String, Value>, String> { let on_override_select: Option<&mut dyn FnMut(Context)> = None; - let overrides: Map<String, Value> = - get_overrides(&query_data, &contexts, &overrides, on_override_select) - .and_then(serde_json::from_value) - .map_err_to_string()?; - merge_overrides_on_default_config(&mut default_config, overrides); + let overrides: Map<String, Value> = get_overrides( + &query_data, + &contexts, + &overrides, + &merge_strategy, + on_override_select, + ) + .and_then(serde_json::from_value) + .map_err_to_string()?; + merge_overrides_on_default_config(&mut default_config, overrides, &merge_strategy); let overriden_config = default_config; Ok(overriden_config) } @@ -82,6 +125,7 @@ pub fn eval_cac_with_reasoning( contexts: &Vec<Context>, overrides: &Map<String, Value>, query_data: &Map<String, Value>, + merge_strategy: MergeStrategy, ) -> Result<Map<String, Value>, String> { let mut reasoning: Vec<Value> = vec![]; @@ -89,6 +133,7 @@ pub fn eval_cac_with_reasoning( &query_data, &contexts, &overrides, + &merge_strategy, Some(&mut |context| { reasoning.push(json!({ "context": context.condition, @@ -99,7 +144,11 @@ pub fn eval_cac_with_reasoning( .and_then(serde_json::from_value) .map_err_to_string()?; - merge_overrides_on_default_config(&mut default_config, applied_overrides); + merge_overrides_on_default_config( + &mut default_config, + applied_overrides, + &merge_strategy, + ); let mut overriden_config = default_config; overriden_config.insert("metadata".into(), json!(reasoning)); Ok(overriden_config) diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 8e727324e..8804984ef 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -16,6 +16,7 @@ use std::{ sync::{Arc, RwLock}, time::{Duration, UNIX_EPOCH}, }; +use strum_macros; use utils::core::MapError; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -31,6 +32,19 @@ pub struct Config { default_configs: Map<String, Value>, } +#[derive(strum_macros::EnumString)] +#[strum(serialize_all = "snake_case")] +pub enum MergeStrategy { + MERGE, + REPLACE, +} + +impl Default for MergeStrategy { + fn default() -> Self { + return Self::MERGE; + } +} + #[derive(Clone)] pub struct Client { tenant: String, @@ -137,13 +151,14 @@ impl Client { self.config.read().map(|c| c.clone()).map_err_to_string() } - pub fn get_last_modified<E>(&'static self) -> Result<DateTime<Utc>, String> { + pub fn get_last_modified(&self) -> Result<DateTime<Utc>, String> { self.last_modified.read().map(|t| *t).map_err_to_string() } pub fn eval( &self, query_data: Map<String, Value>, + merge_strategy: MergeStrategy, ) -> Result<Map<String, Value>, String> { let cac = self.config.read().map_err_to_string()?; eval::eval_cac( @@ -151,6 +166,7 @@ impl Client { &cac.contexts, &cac.overrides, &query_data, + merge_strategy, ) } } diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index da8411fc6..6bb7efbd2 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use super::types::{Config, Context}; use crate::db::schema::{ @@ -8,7 +8,7 @@ use actix_http::header::{HeaderName, HeaderValue}; use actix_web::{ error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope, }; -use cac_client::{eval_cac, eval_cac_with_reasoning}; +use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy}; use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; use diesel::{ dsl::max, @@ -176,6 +176,13 @@ async fn get_resolved_config( }) .collect(); + let merge_strategy = req + .headers() + .get("x-merge-strategy") + .and_then(|header_value: &HeaderValue| header_value.to_str().ok()) + .and_then(|val| MergeStrategy::from_str(val).ok()) + .unwrap_or(MergeStrategy::default()); + let response = if let Some(Value::String(_)) = query_params_map.get("show_reasoning") { HttpResponse::Ok().json( @@ -184,6 +191,7 @@ async fn get_resolved_config( &cac_client_contexts, &res.overrides, &query_params_map, + merge_strategy, ) .map_err_to_internal_server("cac eval failed", Null)?, ) @@ -194,6 +202,7 @@ async fn get_resolved_config( &cac_client_contexts, &res.overrides, &query_params_map, + merge_strategy, ) .map_err_to_internal_server("cac eval failed", Null)?, ) From 75396e5805e93b181ecb09890754d1d31412cc0a Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 4 Mar 2024 10:19:58 +0000 Subject: [PATCH 290/352] chore(version): v0.28.0 [skip ci] --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 4 ++-- crates/cac_client/CHANGELOG.md | 6 ++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df44277cf..35f39ecf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.28.0 - 2024-03-04 +### Package updates +- cac_client bumped to cac_client-v0.6.0 +- context-aware-config bumped to context-aware-config-v0.21.0 +### Global changes +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + ## v0.27.0 - 2024-03-04 ### Package updates - context-aware-config bumped to context-aware-config-v0.20.0 diff --git a/Cargo.lock b/Cargo.lock index 4abd7ba82..db0ab5be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.5.1" +version = "0.6.0" dependencies = [ "actix-web", "chrono", @@ -836,7 +836,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.20.0" +version = "0.21.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 115d38fc5..49158026c 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.6.0 - 2024-03-04 +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + ## cac_client-v0.5.1 - 2024-02-22 #### Bug Fixes - PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index dee898796..fbd224d34 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.5.1" +version = "0.6.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 8c6275253..0ece23649 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.21.0 - 2024-03-04 +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + ## context-aware-config-v0.20.0 - 2024-03-04 #### Features - [PICAF-25877 add node to app directory - (9671875) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 411a6473b..7b15efebf 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.20.0" +version = "0.21.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 6553e30f83acbb0fa3af7182e573721b7d311f23 Mon Sep 17 00:00:00 2001 From: Saurav CV <saurav.cv@juspay.in> Date: Mon, 4 Mar 2024 18:14:37 +0530 Subject: [PATCH 291/352] feat: url click and text wrap fixes --- Cargo.lock | 17 +++++++++-------- crates/frontend/Cargo.toml | 3 ++- crates/frontend/src/pages/Home/Home.rs | 14 +++++++------- crates/frontend/src/utils.rs | 11 +++++++++++ crates/frontend/styles/tailwind.css | 4 ++++ 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db0ab5be2..33e571380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1386,9 +1386,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1427,6 +1427,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "url", "wasm-bindgen", "web-sys", ] @@ -1807,9 +1808,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2632,9 +2633,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -3992,9 +3993,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 0a9fff398..651dbb4de 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -29,6 +29,7 @@ chrono = {workspace = true } strum_macros = { workspace = true } strum = { workspace = true } js-sys = "0.3.65" +url = "2.5.0" [features] @@ -41,4 +42,4 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", -] \ No newline at end of file +] diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 1adb2fa0f..2ee25c02c 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -10,7 +10,7 @@ use crate::{ condition_pills::utils::{extract_and_format, parse_conditions}, context_form::context_form::ContextForm, }, - utils::get_host, + utils::{check_url_and_return_val, get_host}, }; async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { @@ -213,8 +213,8 @@ pub fn home() -> impl IntoView { for (key, value) in config.iter() { table_rows.push_str( format!( - "<tr><td>{key}</td><td>{}</td></tr>", - value.as_str().unwrap() + "<tr><td>{key}</td><td style='word-break: break-word;'>{}</td></tr>", + check_url_and_return_val(value.as_str().unwrap().to_owned()) ) .as_str(), ) @@ -282,9 +282,9 @@ pub fn home() -> impl IntoView { view! { <tr> <td>{config}</td> - <td> + <td style="word-break: break-word;"> {match value { - Value::String(s) => s, + Value::String(s) => check_url_and_return_val(s), Value::Number(num) => num.to_string(), Value::Bool(b) => b.to_string(), _ => "".into(), @@ -347,10 +347,10 @@ pub fn home() -> impl IntoView { < tr > < td > < span name = format!("{unique_name}-1") class = "config-name" class : text - black = { ! striked } class : font - bold = { ! striked } class : text - gray - 300 = { - striked } > { key } </ span > </ td > < td > < span name = + striked } > { key } </ span > </ td > < td style="word-break: break-word;"> < span name = format!("{unique_name}-2") class = "config-value" class : text - black = { ! striked } class : font - bold = { ! - striked } class : text - gray - 300 = { striked } > { value + striked } class : text - gray - 300 = { striked } > { check_url_and_return_val(value) } </ span > </ td > </ tr > }, ) diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index 942439222..e6a5cdb34 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -3,6 +3,7 @@ use std::env; use crate::types::Envs; use leptos::*; use serde_json::Value; +use url::Url; use wasm_bindgen::JsCast; pub fn modal_action(name: &str, action: &str) { @@ -263,3 +264,13 @@ pub fn extract_conditions( Ok(condition_tuples) } + +pub fn check_url_and_return_val(s: String) -> String { + match Url::parse(&s) { + Ok(_) => format!( + "<a class='value_link' href={} target='_blank'>{}</a>", + &s, &s + ), + Err(_) => s, + } +} diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index cbc7d6047..d6bfa4fd6 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -21,4 +21,8 @@ .context_condition{ font-size: 0.9rem; +} + +.value_link { + text-decoration:underline; } \ No newline at end of file From 50c8a9605f20bc9e3d47e9fa377bccb7804e2688 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 29 Feb 2024 19:20:33 +0530 Subject: [PATCH 292/352] fix: added drawer, improved UX & single click override addition to variants --- crates/frontend/src/app.rs | 2 +- .../frontend/src/components/button/button.rs | 13 +- .../components/context_form/context_form.rs | 311 +++++++++--------- .../frontend/src/components/drawer/drawer.rs | 73 ++++ crates/frontend/src/components/drawer/mod.rs | 1 + .../src/components/dropdown/dropdown.rs | 115 +++++++ .../frontend/src/components/dropdown/mod.rs | 2 + .../frontend/src/components/dropdown/utils.rs | 4 + .../experiment_form/experiment_form.rs | 206 ++++-------- .../src/components/experiment_form/utils.rs | 9 + crates/frontend/src/components/mod.rs | 3 + .../components/override_form/override_form.rs | 139 ++++---- .../src/components/variant_form/mod.rs | 1 + .../components/variant_form/variant_form.rs | 290 ++++++++++++++++ .../frontend/src/pages/ExperimentList/mod.rs | 2 - .../experiment_list.rs} | 108 ++---- .../frontend/src/pages/experiment_list/mod.rs | 2 + .../utils.rs | 0 crates/frontend/src/pages/mod.rs | 2 +- crates/frontend/src/types.rs | 20 ++ crates/frontend/src/utils.rs | 6 +- crates/frontend/styles/tailwind.css | 46 +++ 22 files changed, 870 insertions(+), 485 deletions(-) create mode 100644 crates/frontend/src/components/drawer/drawer.rs create mode 100644 crates/frontend/src/components/drawer/mod.rs create mode 100644 crates/frontend/src/components/dropdown/dropdown.rs create mode 100644 crates/frontend/src/components/dropdown/mod.rs create mode 100644 crates/frontend/src/components/dropdown/utils.rs create mode 100644 crates/frontend/src/components/variant_form/mod.rs create mode 100644 crates/frontend/src/components/variant_form/variant_form.rs delete mode 100644 crates/frontend/src/pages/ExperimentList/mod.rs rename crates/frontend/src/pages/{ExperimentList/ExperimentList.rs => experiment_list/experiment_list.rs} (65%) create mode 100644 crates/frontend/src/pages/experiment_list/mod.rs rename crates/frontend/src/pages/{ExperimentList => experiment_list}/utils.rs (100%) diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 12fe59645..8e489f52a 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -4,8 +4,8 @@ use leptos_router::*; use serde_json::json; use crate::hoc::layout::layout::Layout; +use crate::pages::experiment_list::experiment_list::ExperimentList; use crate::pages::Dimensions::Dimensions::Dimensions; -use crate::pages::ExperimentList::ExperimentList::ExperimentList; use crate::pages::{ ContextOverride::ContextOverride::ContextOverride, DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, diff --git a/crates/frontend/src/components/button/button.rs b/crates/frontend/src/components/button/button.rs index bec5c76bc..ad4d5bf39 100644 --- a/crates/frontend/src/components/button/button.rs +++ b/crates/frontend/src/components/button/button.rs @@ -2,10 +2,19 @@ use leptos::*; use web_sys::MouseEvent; #[component] -pub fn button<F: Fn(MouseEvent) + 'static>(text: String, on_click: F) -> impl IntoView { +pub fn button<F: Fn(MouseEvent) + 'static>( + text: String, + on_click: F, + #[prop(default = String::new())] class: String, + #[prop(default = String::new())] id: String, +) -> impl IntoView { view! { <button - class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" + class=format!( + "btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 {class}", + ) + + id=id on:click=on_click > {text} diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 205bdb4d6..5a9e162c8 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -1,6 +1,6 @@ +use crate::components::dropdown::dropdown::{Dropdown, DropdownDirection}; use crate::types::Dimension; use leptos::*; -use std::cmp; use std::collections::HashSet; use web_sys::MouseEvent; @@ -10,6 +10,7 @@ pub fn context_form<NF>( dimensions: Vec<Dimension>, is_standalone: bool, context: Vec<(String, String, String)>, + #[prop(default = String::new())] heading_sub_text: String, #[prop(default = false)] disabled: bool, ) -> impl IntoView where @@ -20,10 +21,9 @@ where let (context, set_context) = create_signal(context.clone()); let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); - let last_idx = create_memo(move |_| { - let len = context.get().len(); - cmp::max(0, len - 1) - }); + let dimensions = StoredValue::new(dimensions); + + let last_idx = create_memo(move |_| context.get().len().max(1) - 1); let on_click = move |event: MouseEvent| { event.prevent_default(); @@ -36,191 +36,174 @@ where handle_change(f_context.clone()); }); - let add_dropdown_class = if disabled { - "dropdown dropdown-left disable-click" - } else { - "dropdown dropdown-left" + let handle_select_dropdown_option = move |selected_dimension: Dimension| { + let dimension_name = selected_dimension.dimension; + set_context.update(|value| { + leptos::logging::log!("{:?}", value); + value.push((dimension_name.to_string(), "".to_string(), "".to_string())) + }); + set_used_dimensions.update(|value: &mut HashSet<String>| { + value.insert(dimension_name.to_string()); + }); }; view! { <div> <div class="form-control w-full "> - <div class="flex gap-4 justify-between"> - <label class="label"> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> <span class="label-text font-semibold text-base">Context</span> + <span class="label-text text-slate-400">{heading_sub_text}</span> </label> - <div> - <div class=add_dropdown_class> - <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> - <i class="ri-add-line"></i> - Add Context - </label> - <ul - tabindex="0" - class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" - > - <For - each=move || { - dimensions - .clone() - .into_iter() - .filter(|dim| { - !used_dimensions.get().contains(&dim.dimension) - }) - .collect::<Vec<Dimension>>() - } - - key=|dimension: &Dimension| dimension.dimension.to_string() - children=move |dimension: Dimension| { - let dimension_name = dimension.dimension.to_string(); - let label = dimension_name.to_string(); - view! { - <li on:click=move |_| { - set_context - .update(|value| { - leptos::logging::log!("{:?}", value); - value - .push(( - dimension_name.to_string(), - "".to_string(), - "".to_string(), - )) - }); - set_used_dimensions - .update(|value: &mut HashSet<String>| { - value.insert(dimension_name.to_string()); - }); - }> - - <a>{label.to_string()}</a> - </li> - } - } - /> - - </ul> - </div> - </div> </div> - <Show when=move || context.get().len() == 0> - <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> - <div> - <i class="ri-add-circle-line text-xl"></i> - </div> - <div> - <span class="text-semibold text-sm">Add Context</span> - </div> - </div> - </Show> - <div class="p-4"> - <For - each=move || { - context - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, (String, String, String))>>() - } - - key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) - children=move |(idx, (dimension, operator, value))| { - let dimension_label = dimension.to_string(); - let dimension_name = dimension.to_string(); - view! { - <div class="flex gap-x-6"> - <div class="form-control w-20"> - <label class="label font-medium font-mono text-sm"> - <span class="label-text">Operator</span> - </label> - <select - disabled=disabled - value=operator.clone() - on:input=move |event| { - let input_value = event_target_value(&event); - set_context - .update(|curr_context| { - curr_context[idx].1 = input_value; - }); - } - - name="context-dimension-operator" - class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" - > - <option disabled selected> - Pick one - </option> - <option value="==" selected=operator.clone() == "=="> - "IS" - </option> - <option value="IN" selected=operator.clone() == "IN"> - "HAS" - </option> - <option value="<=" selected=operator.clone() == "<="> - "BETWEEN (inclusive)" - </option> - </select> + <div class="card w-full bg-slate-50"> + <div class="card-body"> + <Show when=move || context.get().len() == 0> + <div class="flex justify-center"> + <Dropdown + dropdown_width="w-80" + dropdown_icon="ri-add-line".to_string() + dropdown_text="Add Context".to_string() + dropdown_direction=DropdownDirection::Left + dropdown_options=dimensions.get_value() + disabled=disabled + on_select=Box::new(handle_select_dropdown_option) + /> + </div> + </Show> + <For + each=move || { + context + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, String, String))>>() + } - </div> - <div class="form-control"> - <label class="label font-mono text-sm"> - <span class="label-text" name="context-dimension-name"> - {dimension_label} - </span> - </label> - <div class="flex gap-x-6 items-center"> - <input + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) + children=move |(idx, (dimension, operator, value))| { + let dimension_label = dimension.to_string(); + let dimension_name = dimension.to_string(); + view! { + <div class="flex gap-x-6"> + <div class="form-control w-20"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">Operator</span> + </label> + <select disabled=disabled - value=value + value=operator.clone() on:input=move |event| { let input_value = event_target_value(&event); set_context .update(|curr_context| { - curr_context[idx].2 = input_value; + curr_context[idx].1 = input_value; }); } - name="context-dimension-value" - type="text" - placeholder="Type here" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - /> - <button - class="btn btn-ghost btn-circle btn-sm" - disabled=disabled - on:click=move |_| { - set_context - .update(|value| { - value.remove(idx); - }); - set_used_dimensions - .update(|value| { - value.remove(&dimension_name); - }); - } + name="context-dimension-operator" + class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" > + <option disabled selected> + Pick one + </option> + <option value="==" selected=operator.clone() == "=="> + "IS" + </option> + <option value="IN" selected=operator.clone() == "IN"> + "HAS" + </option> + <option value="<=" selected=operator.clone() == "<="> + "BETWEEN (inclusive)" + </option> + </select> - <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> - </button> + </div> + <div class="form-control"> + <label class="label font-mono text-sm"> + <span class="label-text" name="context-dimension-name"> + {dimension_label} + </span> + </label> + <div class="flex gap-x-6 items-center"> + <input + disabled=disabled + value=value + on:input=move |event| { + let input_value = event_target_value(&event); + set_context + .update(|curr_context| { + curr_context[idx].2 = input_value; + }); + } + + name="context-dimension-value" + type="text" + placeholder="Type here" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + /> + <button + class="btn btn-ghost btn-circle btn-sm" + disabled=disabled + on:click=move |_| { + let mut current_context = context.get(); + current_context.remove(idx); + set_context.set(current_context); + set_used_dimensions + .update(|value| { + value.remove(&dimension_name); + }); + } + > + + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> + </div> </div> </div> - </div> - {move || { - if last_idx.get() != idx { - view! { - <div class="my-3 ml-5 ml-6 ml-7"> - <span class="font-mono text-xs">"&&"</span> - </div> + {move || { + if last_idx.get() != idx { + view! { + <div class="my-3 ml-5 ml-6 ml-7"> + <span class="font-mono text-xs">"&&"</span> + </div> + } + .into_view() + } else { + view! {}.into_view() } - .into_view() - } else { - view! {}.into_view() + }} + } + } + /> + + <Show when=move || context.get().len() != 0> + <div class="mt-4"> + + {move || { + let dimensions = dimensions + .get_value() + .into_iter() + .filter(|dimension| { + !used_dimensions.get().contains(&dimension.dimension) + }) + .collect::<Vec<Dimension>>(); + view! { + <Dropdown + dropdown_icon="ri-add-line".to_string() + dropdown_text="Add Context".to_string() + dropdown_options=dimensions + disabled=disabled + on_select=Box::new(handle_select_dropdown_option) + /> } }} - } - } - /> + </div> + </Show> + + </div> </div> </div> <Show when=move || is_standalone> diff --git a/crates/frontend/src/components/drawer/drawer.rs b/crates/frontend/src/components/drawer/drawer.rs new file mode 100644 index 000000000..f7dd45e8a --- /dev/null +++ b/crates/frontend/src/components/drawer/drawer.rs @@ -0,0 +1,73 @@ +use leptos::*; + +use crate::utils::get_element_by_id; + +pub fn open_drawer(id: &str) { + match get_element_by_id::<web_sys::HtmlInputElement>(id) { + Some(ele) => ele.set_checked(true), + None => { + logging::log!("{} drawer checkbox not found", id); + } + }; +} + +pub fn close_drawer(id: &str) { + match get_element_by_id::<web_sys::HtmlInputElement>(id) { + Some(ele) => ele.set_checked(false), + None => { + logging::log!("{} drawer checkbox not found", id); + } + }; +} + +#[component] +pub fn drawer_btn(drawer_id: String, children: Children) -> impl IntoView { + let open_drawer_id = drawer_id.clone(); + view! { + <button + class=format!( + "btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 drawer-button", + ) + + id=format!("{}-btn", drawer_id.clone()) + on:click=move |_| { open_drawer(&open_drawer_id) } + > + {children()} + </button> + } +} + +#[component] +pub fn drawer( + id: String, + children: Children, + #[prop(default = "")] header: &'static str, + #[prop(default = "w-[60vw]")] drawer_width: &'static str, +) -> impl IntoView { + let close_drawer_id = id.clone(); + + view! { + <div class="drawer drawer-end"> + <input id=id.clone() type="checkbox" class="drawer-toggle"/> + + <div class="drawer-side drawer-zindex w-full"> + <label for=id.clone() class="drawer-overlay"></label> + <div class=format!( + "min-h-full {drawer_width} bg-base-100 overflow-x-hidden overflow-y-auto", + )> + <div class="px-4 py-4 flex justify-between items-center"> + <h3 class="text-lg font-bold">{header}</h3> + <button + class="btn btn-sm btn-circle btn-ghost" + on:click=move |_| { close_drawer(&close_drawer_id) } + > + <i class="ri-close-line"></i> + </button> + </div> + <div class="divider mt-0"></div> + <div class="p-4">{children()}</div> + </div> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/drawer/mod.rs b/crates/frontend/src/components/drawer/mod.rs new file mode 100644 index 000000000..150dae562 --- /dev/null +++ b/crates/frontend/src/components/drawer/mod.rs @@ -0,0 +1 @@ +pub mod drawer; diff --git a/crates/frontend/src/components/dropdown/dropdown.rs b/crates/frontend/src/components/dropdown/dropdown.rs new file mode 100644 index 000000000..40faf7fdd --- /dev/null +++ b/crates/frontend/src/components/dropdown/dropdown.rs @@ -0,0 +1,115 @@ +use leptos::*; + +use super::utils::DropdownOption; + +#[derive(PartialEq)] +pub enum DropdownBtnType { + Outline, + Link, + Fill, +} + +#[derive(PartialEq)] +pub enum DropdownDirection { + Right, + Left, +} + +#[component] +pub fn dropdown<T>( + dropdown_text: String, + dropdown_icon: String, + dropdown_options: Vec<T>, + on_select: Box<dyn Fn(T)>, + #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, + #[prop(default = DropdownBtnType::Outline)] dropdown_btn_type: DropdownBtnType, + #[prop(default = "w-96")] dropdown_width: &'static str, + #[prop(default = false)] disabled: bool, + #[prop(default = true)] searchable: bool, +) -> impl IntoView +where + T: DropdownOption + Clone + 'static, +{ + let all_options = StoredValue::new(dropdown_options.clone()); + let (search_term, set_search_term) = create_signal(String::new()); + let dropdown_options = Signal::derive(move || { + let term = search_term.get(); + all_options + .get_value() + .into_iter() + .filter(|option| option.label().contains(&term)) + .collect::<Vec<T>>() + }); + let on_select = StoredValue::new(on_select); + + view! { + <div + class="dropdown" + class=("disable-click", disabled) + class=("dropdown-right", dropdown_direction == DropdownDirection::Right) + class=("dropdown-left", dropdown_direction == DropdownDirection::Left) + > + <label + tabindex="0" + class="btn btn-sm text-xs m-1 w-full" + class=("btn-purple-outline", dropdown_btn_type == DropdownBtnType::Outline) + class=("btn-purple-link", dropdown_btn_type == DropdownBtnType::Link) + > + <i class=format!("{dropdown_icon}")></i> + {dropdown_text} + </label> + <ul + tabindex="0" + class=format!( + "{dropdown_width} dropdown-content z-[1] menu flex-nowrap p-2 shadow bg-base-100 rounded-box max-h-96 overflow-y-scroll overflow-x-hidden", + ) + > + + {move || { + if searchable { + view! { + <div class="mb-3"> + <label class="input input-bordered flex items-center gap-2 h-10"> + <i class="ri-search-line"></i> + <input + type="text" + class="grow" + placeholder="Search" + on:input=move |event| { + set_search_term.set(event_target_value(&event)); + } + /> + + </label> + </div> + } + .into_view() + } else { + view! {}.into_view() + } + }} + + <For + each=move || dropdown_options.get() + key=|option: &T| option.key() + children=move |option: T| { + let label = option.label(); + view! { + <li + class="w-full" + on:click=move |_| { + let selected_option = option.clone(); + on_select.with_value(|f| f(selected_option)); + } + > + + <a class="w-full word-break-break">{label.to_string()}</a> + </li> + } + } + /> + + </ul> + </div> + } +} diff --git a/crates/frontend/src/components/dropdown/mod.rs b/crates/frontend/src/components/dropdown/mod.rs new file mode 100644 index 000000000..53d6d0629 --- /dev/null +++ b/crates/frontend/src/components/dropdown/mod.rs @@ -0,0 +1,2 @@ +pub mod dropdown; +pub mod utils; diff --git a/crates/frontend/src/components/dropdown/utils.rs b/crates/frontend/src/components/dropdown/utils.rs new file mode 100644 index 000000000..e39b36bf9 --- /dev/null +++ b/crates/frontend/src/components/dropdown/utils.rs @@ -0,0 +1,4 @@ +pub trait DropdownOption { + fn key(&self) -> String; + fn label(&self) -> String; +} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs index d4afd1986..e860fe9d6 100644 --- a/crates/frontend/src/components/experiment_form/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -1,12 +1,10 @@ use super::utils::{create_experiment, update_experiment}; use crate::components::button::button::Button; -use crate::components::{ - context_form::context_form::ContextForm, override_form::override_form::OverrideForm, -}; +use crate::components::context_form::context_form::ContextForm; +use crate::components::variant_form::variant_form::VariantForm; use crate::types::{DefaultConfig, Dimension, Variant, VariantType}; -use chrono::offset::Local; use leptos::*; -use serde_json::{Map, Value}; +use serde_json::Map; use web_sys::MouseEvent; fn default_variants_for_form() -> Vec<(String, Variant)> { @@ -34,6 +32,19 @@ fn default_variants_for_form() -> Vec<(String, Variant)> { ] } +fn get_init_state(variants: &[Variant]) -> Vec<(String, Variant)> { + let init_variants = if variants.len() == 0 { + default_variants_for_form() + } else { + variants + .into_iter() + .map(|variant| (variant.id.to_string(), variant.clone())) + .collect::<Vec<(String, Variant)>>() + }; + + init_variants +} + #[component] pub fn experiment_form<NF>( #[prop(default = false)] edit: bool, @@ -48,35 +59,23 @@ pub fn experiment_form<NF>( where NF: Fn() + 'static + Clone, { + let init_variants = get_init_state(&variants); + let default_config = StoredValue::new(default_config); let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let default_variants = if variants.len() == 0 { - default_variants_for_form() - } else { - variants - .into_iter() - .map(|variant| (variant.id.to_string(), variant)) - .collect::<Vec<(String, Variant)>>() - }; - let (experiment_name, set_experiment_name) = create_signal(name); let (f_context, set_context) = create_signal(context.clone()); - let (f_variants, set_variants) = create_signal(default_variants); + let (f_variants, set_variants) = create_signal(init_variants); let handle_context_form_change = move |updated_ctx: Vec<(String, String, String)>| { - set_context.set(updated_ctx); + set_context.set_untracked(updated_ctx); }; - let handle_override_form_change = move |variant_idx: usize| { - let handle_change = move |updated_overrides: Map<String, Value>| { - set_variants.update(|curr_variants| { - curr_variants[variant_idx].1.overrides = updated_overrides.clone(); - }); - }; - handle_change + let handle_variant_form_change = move |updated_varaints: Vec<(String, Variant)>| { + set_variants.set_untracked(updated_varaints); }; - let dimension_on_submit = dimensions.clone(); + let dimensions = StoredValue::new(dimensions); let on_submit = move |event: MouseEvent| { event.prevent_default(); logging::log!("Submitting experiment form"); @@ -92,7 +91,6 @@ where let tenant = tenant_rs.get(); let experiment_id = id.clone(); let handle_submit_clone = handle_submit.clone(); - let dimensions = dimension_on_submit.clone(); logging::log!("{:?}", f_experiment_name); logging::log!("{:?}", f_context); @@ -107,7 +105,7 @@ where f_variants, f_experiment_name, tenant, - dimensions.clone(), + dimensions.get_value(), ) .await }; @@ -129,7 +127,7 @@ where <div> <div class="form-control w-full"> <label class="label"> - <span class="label-text">Name</span> + <span class="label-text">Experiment Name</span> </label> <input disabled=edit @@ -139,137 +137,45 @@ where name="expName" id="expName" placeholder="ex: testing hyperpay release" - class="input input-bordered w-full max-w-xs" + class="input input-bordered w-full max-w-md" /> </div> - <div class="my-4"> - <ContextForm - dimensions=dimensions.clone() - context=context - handle_change=handle_context_form_change - is_standalone=false - disabled=edit - /> - </div> - - <div class="form-control w-full"> - <div class="flex items-center justify-between gap-4"> - <label class="label"> - <span class="label-text font-semibold text-base">Variants</span> - </label> - - <button - class="btn btn-outline btn-sm text-xs m-1" - disabled=edit - on:click:undelegated=move |_| { - leptos::logging::log!("add new variant"); - set_variants - .update(|curr_variants| { - let total_variants = curr_variants.len(); - let key = Local::now().timestamp().to_string(); - curr_variants - .push(( - key, - Variant { - id: format!("variant-{}", total_variants), - variant_type: VariantType::EXPERIMENTAL, - context_id: None, - override_id: None, - overrides: Map::new(), - }, - )) - }); - } - > + <div class="divider"></div> - <i class="ri-add-line"></i> - Add Variant - </button> - </div> - <For - each=move || { - f_variants - .get() - .into_iter() - .enumerate() - .collect::<Vec<(usize, (String, Variant))>>() + <div class="my-4"> + {move || { + let context = f_context.get(); + view! { + <ContextForm + dimensions=dimensions.get_value() + context=context + handle_change=handle_context_form_change + is_standalone=false + disabled=edit + heading_sub_text=String::from( + "Define rules under which this experiment would run", + ) + /> } + }} - key=|(_, (key, _))| key.to_string() - children=move |(idx, (_, variant))| { - let variant_clone = variant.clone(); - let default_config = default_config.clone(); - let handle_change = handle_override_form_change(idx); - view! { - <div class="my-2 p-4 rounded bg-gray-50"> - <div class="flex items-center gap-4"> - <div class="form-control w-1/3"> - <label class="label"> - <span class="label-text">ID</span> - </label> - <input - name="variantId" - value=move || variant.id.to_string() - disabled=edit - type="text" - placeholder="Type a unique name here" - class="input input-bordered w-full max-w-xs" - /> - </div> - <div class="form-control w-1/3"> - <label class="label font-medium text-sm"> - <span class="label-text">Type</span> - </label> - <select - name="expType[]" - value=variant.variant_type.to_string() - disabled=edit - on:change=move |ev| { - let mut new_variant = variant_clone.clone(); - new_variant - .variant_type = match event_target_value(&ev).as_str() { - "CONTROL" => VariantType::CONTROL, - _ => VariantType::EXPERIMENTAL, - }; - set_variants - .update(|value| { - value[idx].1 = new_variant; - }) - } + </div> - class="select select-bordered" - > - <option disabled>Pick one</option> - <option - value=VariantType::CONTROL.to_string() - selected=VariantType::CONTROL == variant.variant_type - > - {VariantType::CONTROL.to_string()} - </option> - <option - value=VariantType::EXPERIMENTAL.to_string() - selected=VariantType::EXPERIMENTAL == variant.variant_type - > - {VariantType::EXPERIMENTAL.to_string()} - </option> - </select> - </div> - </div> - <div class="mt-2"> - <OverrideForm - overrides=variant.overrides - default_config=default_config - handle_change=handle_change - is_standalone=false - /> - </div> - </div> - } - } - /> + <div class="divider"></div> + + {move || { + let variants = f_variants.get(); + view! { + <VariantForm + edit=edit + variants=variants + default_config=default_config.get_value() + handle_change=handle_variant_form_change + /> + } + }} - </div> <div class="flex justify-end mt-8"> <Button text="Submit".to_string() on_click=on_submit/> </div> diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index 81a4f3668..cc1f661ed 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -7,6 +7,13 @@ use crate::utils::get_host; use reqwest::StatusCode; use serde_json::json; +pub fn validate_experiment(experiment: &ExperimentCreateRequest) -> Result<bool, String> { + if experiment.name.is_empty() { + return Err(String::from("experiment name should not be empty")); + } + Ok(true) +} + pub async fn create_experiment( conditions: Vec<(String, String, String)>, variants: Vec<Variant>, @@ -20,6 +27,8 @@ pub async fn create_experiment( context: construct_context(conditions, dimensions), }; + let _ = validate_experiment(&payload)?; + let client = reqwest::Client::new(); let host = get_host(); let url = format!("{host}/experiments"); diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index 00c0794c8..fe02ca5bb 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -3,6 +3,8 @@ pub mod condition_pills; pub mod context_form; pub mod default_config_form; pub mod dimension_form; +pub mod drawer; +pub mod dropdown; pub mod experiment; pub mod experiment_conclude_form; pub mod experiment_form; @@ -14,3 +16,4 @@ pub mod pagination; pub mod side_nav; pub mod stat; pub mod table; +pub mod variant_form; diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 0e629f3d9..1464e1b27 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,8 +1,9 @@ -use crate::types::DefaultConfig; +use crate::{ + components::dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + types::DefaultConfig, +}; use leptos::*; use serde_json::{json, Map, Value}; -use std::collections::HashSet; -use std::rc::Rc; use std::str::FromStr; use web_sys::MouseEvent; @@ -24,29 +25,32 @@ pub fn override_form<NF>( default_config: Vec<DefaultConfig>, handle_change: NF, is_standalone: bool, + #[prop(default = false)] disable_remove: bool, + #[prop(default = true)] show_add_override: bool, + #[prop(into, default = None)] handle_key_remove: Option<Callback<String, ()>>, ) -> impl IntoView where NF: Fn(Map<String, Value>) + 'static, { - let has_default_config = default_config.len() != 0; - let (used_config_keys, set_used_config_keys) = create_signal( - overrides - .keys() - .map(String::from) - .collect::<HashSet<String>>(), - ); + let default_config = StoredValue::new(default_config); let (overrides, set_overrides) = create_signal(overrides.clone()); + let unused_config_keys = Signal::derive(move || { + default_config + .get_value() + .into_iter() + .filter(|config| !overrides.get().contains_key(&config.key)) + .collect::<Vec<DefaultConfig>>() + }); + let has_default_config = Signal::derive(move || unused_config_keys.get().len() > 0); let on_submit = move |event: MouseEvent| { event.prevent_default(); logging::log!("{:?}", overrides.get()); }; - let default_config_rc = Rc::new(default_config.clone()); - let default_config_value = - |name: &str, val: &str, default_configs: &Vec<DefaultConfig>| { - let dimension_type = get_default_config_type(default_configs.clone(), name); + |name: &str, val: &str, default_config: &Vec<DefaultConfig>| { + let dimension_type = get_default_config_type(default_config.clone(), name); match dimension_type.replace("\"", "").as_str() { "boolean" => match bool::from_str(val) { Ok(boolean) => Value::Bool(boolean), @@ -60,6 +64,13 @@ where } }; + let handle_config_key_select = move |default_config: DefaultConfig| { + let config_key = default_config.key; + set_overrides.update(|value| { + value.insert(config_key.to_string(), json!("")); + }); + }; + create_effect(move |_| { let f_override = overrides.get(); handle_change(f_override.clone()); @@ -72,53 +83,16 @@ where <label class="label"> <span class="label-text font-semibold text-base">Overrides</span> </label> - <div> - <div class="dropdown dropdown-left"> - <label tabindex="0" class="btn btn-outline btn-sm text-xs m-1"> - <i class="ri-add-line"></i> - Add Override - </label> - <ul - tabindex="0" - class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" - > - <Show when=move || !has_default_config>No default config</Show> - <For - each=move || { - default_config - .clone() - .into_iter() - .filter(|item| { - !used_config_keys.get().contains(&item.key) - }) - .collect::<Vec<DefaultConfig>>() - } - - key=|item: &DefaultConfig| item.key.to_string() - children=move |item: DefaultConfig| { - let config_key = item.key.to_string(); - let label = config_key.to_string(); - view! { - <li on:click=move |_| { - set_overrides - .update(|value| { - value.insert(config_key.to_string(), json!("")); - }); - set_used_config_keys - .update(|value: &mut HashSet<String>| { - value.insert(config_key.to_string()); - }); - }> - - <a>{label.to_string()}</a> - </li> - } - } - /> - - </ul> - </div> - </div> + <Show when=move || show_add_override> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + </Show> </div> <Show when=move || overrides.get().len() == 0> <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> @@ -137,7 +111,6 @@ where let config_key_label = config_key.to_string(); let config_key_value = config_key.to_string(); let config_value = config_value.to_string().replace("\"", ""); - let default_config_clone = default_config_rc.clone(); view! { <div> <div class="flex items-center gap-4"> @@ -158,7 +131,7 @@ where let default_config_val = default_config_value( &config_key_value, &input_value, - &default_config_clone.clone(), + &default_config.get_value(), ); logging::log!("Default Config: {}", default_config_val); set_overrides @@ -174,23 +147,33 @@ where </div> <div class="w-1/5"> - <button - class="btn btn-ghost btn-circle btn-sm" - on:click=move |ev| { - ev.prevent_default(); - set_overrides - .update(|value| { - value.remove(&config_key); - }); - set_used_config_keys - .update(|value| { - value.remove(&config_key); - }); + + {if !disable_remove { + view! { + <button + class="btn btn-ghost btn-circle btn-sm" + on:click=move |ev| { + ev.prevent_default(); + match handle_key_remove { + Some(f) => f.call(config_key.clone()), + None => { + set_overrides + .update(|value| { + value.remove(&config_key); + }) + } + }; + } + > + + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> } - > + .into_view() + } else { + view! {}.into_view() + }} - <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> - </button> </div> </div> </div> diff --git a/crates/frontend/src/components/variant_form/mod.rs b/crates/frontend/src/components/variant_form/mod.rs new file mode 100644 index 000000000..9170f7f28 --- /dev/null +++ b/crates/frontend/src/components/variant_form/mod.rs @@ -0,0 +1 @@ +pub mod variant_form; diff --git a/crates/frontend/src/components/variant_form/variant_form.rs b/crates/frontend/src/components/variant_form/variant_form.rs new file mode 100644 index 000000000..42ae91b93 --- /dev/null +++ b/crates/frontend/src/components/variant_form/variant_form.rs @@ -0,0 +1,290 @@ +use std::collections::HashSet; + +use chrono::Local; +use leptos::*; +use serde_json::{json, Map, Value}; + +use crate::{ + components::{ + dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + override_form::override_form::OverrideForm, + }, + types::{DefaultConfig, Variant, VariantType}, +}; + +fn get_override_keys_from_variants(variants: &[(String, Variant)]) -> HashSet<String> { + variants + .iter() + .map(|(_, variant)| { + variant + .overrides + .keys() + .map(String::from) + .collect::<Vec<String>>() + }) + .flatten() + .collect::<HashSet<String>>() +} + +fn get_init_state(variants: &[(String, Variant)]) -> HashSet<String> { + let init_override_keys = get_override_keys_from_variants(variants); + + init_override_keys +} + +#[component] +pub fn variant_form<HC>( + edit: bool, + variants: Vec<(String, Variant)>, + default_config: Vec<DefaultConfig>, + handle_change: HC, +) -> impl IntoView +where + HC: Fn(Vec<(String, Variant)>) + 'static + Clone, +{ + let init_override_keys = get_init_state(&variants); + let (f_variants, set_variants) = create_signal(variants); + let (override_keys, set_override_keys) = create_signal(init_override_keys); + + let default_config = StoredValue::new(default_config); + let handle_change = StoredValue::new(handle_change); + let unused_config_keys = Signal::derive(move || { + default_config + .get_value() + .into_iter() + .filter(|config| !override_keys.get().contains(&config.key)) + .collect::<Vec<DefaultConfig>>() + }); + + let handle_control_override_key_remove = move |removed_key: String| { + logging::log!("Removing key {:?}", removed_key); + + set_override_keys.update(|current_override_keys| { + current_override_keys.remove(&removed_key); + }); + set_variants.update(|current_variants| { + for variant in current_variants.iter_mut() { + variant.1.overrides.remove(&removed_key); + } + }); + }; + + let handle_override_form_change = move |variant_idx: usize| { + let callback = move |updated_overrides: Map<String, Value>| { + set_variants.update_untracked(|curr_variants| { + curr_variants[variant_idx].1.overrides = updated_overrides.clone(); + handle_change.get_value()(curr_variants.clone()); + }); + }; + callback + }; + + create_effect(move |_| { + let f_variants = f_variants.get(); + handle_change.get_value()(f_variants.clone()); + }); + + let handle_config_key_select = move |default_config: DefaultConfig| { + let config_key = default_config.key; + set_variants.update(|current_variants: &mut Vec<(String, Variant)>| { + for (_, variant) in current_variants.iter_mut() { + variant.overrides.insert(config_key.clone(), json!("")); + } + }); + set_override_keys.update(|value: &mut HashSet<String>| { + value.insert(config_key.clone()); + }); + }; + + view! { + <div class="form-control w-full"> + <div class="flex items-center justify-between gap-4"> + <label class="label flex-col justify-center items-start"> + <span class="label-text font-semibold text-base">Experiment Variants</span> + <span class="label-text text-slate-400"> + "These are the override sets that would apply on the above context" + </span> + </label> + + </div> + <For + each=move || { + f_variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, Variant))>>() + } + + key=|(_, (key, _))| key.to_string() + children=move |(idx, (_, variant))| { + let is_control_variant = variant.variant_type == VariantType::CONTROL; + let handle_change = handle_override_form_change(idx); + let variant_type_label = match variant.variant_type { + VariantType::CONTROL => "Control".to_string(), + VariantType::EXPERIMENTAL => format!("Variant {idx}"), + }; + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center justify-between"> + <label class="label"> + <span class="label-text font-semibold text-base"> + {variant_type_label} + </span> + </label> + <Show when=move || { + is_control_variant && override_keys.get().len() > 0 + }> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + </Show> + </div> + <div class="flex items-center gap-4 my-4"> + <div class="form-control"> + <label class="label"> + <span class="label-text">ID</span> + </label> + </div> + <div class="form-control w-2/5"> + <input + name="variantId" + value=move || variant.id.to_string() + disabled=edit + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs h-10" + on:input=move |event| { + let variant_id = event_target_value(&event); + set_variants + .update(|current_variants: &mut Vec<(String, Variant)>| { + let variant_to_be_updated = current_variants.get_mut(idx); + match variant_to_be_updated { + Some((_, ref mut variant)) => { + variant.id = variant_id; + } + None => { + logging::log!( + "variant not found to update with id: {:?}", variant_id + ) + } + } + }); + } + /> + + </div> + </div> + <div class="mt-2"> + <Show when=move || { + is_control_variant && override_keys.get().len() == 0 + }> + <div class="my-4 flex flex-col justify-between items-center"> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + <div> + <span class="label-text text-slate-400 text-sm"> + "Add keys from your config that you want to override in this experiment" + </span> + </div> + </div> + </Show> + + <Show when=move || { + !is_control_variant && override_keys.get().len() == 0 + }> + <div class="my-4 flex flex-col justify-between items-center"> + <span class="label-text text-slate-400 text-sm"> + "Keys added in CONTROL will appear here as well for override" + </span> + </div> + </Show> + + <Show when=move || { + override_keys.get().len() > 0 + }> + {move || { + let variant = f_variants.get().get(idx).unwrap().clone(); + let overrides = variant.1.overrides; + if is_control_variant { + view! { + <OverrideForm + overrides=overrides + default_config=default_config.get_value() + handle_change=handle_change + is_standalone=false + show_add_override=false + handle_key_remove=Some( + Callback::new(handle_control_override_key_remove), + ) + /> + } + } else { + view! { + <OverrideForm + overrides=overrides + default_config=default_config.get_value() + handle_change=handle_change + is_standalone=false + show_add_override=false + disable_remove=true + /> + } + } + }} + + </Show> + + </div> + </div> + } + } + /> + + <div> + <button + class="btn btn-purple-outline btn-sm text-xs m-1" + disabled=edit + on:click:undelegated=move |_| { + leptos::logging::log!("add new variant"); + set_variants + .update(|curr_variants| { + let total_variants = curr_variants.len(); + let key = Local::now().timestamp().to_string(); + let overrides = Map::from_iter( + override_keys.get().into_iter().map(|key| { (key, json!("")) }), + ); + curr_variants + .push(( + key, + Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: overrides, + }, + )) + }); + } + > + + <i class="ri-add-line"></i> + Add Variant + </button> + </div> + + </div> + } +} diff --git a/crates/frontend/src/pages/ExperimentList/mod.rs b/crates/frontend/src/pages/ExperimentList/mod.rs deleted file mode 100644 index bbb7196fb..000000000 --- a/crates/frontend/src/pages/ExperimentList/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod ExperimentList; -pub mod utils; diff --git a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs b/crates/frontend/src/pages/experiment_list/experiment_list.rs similarity index 65% rename from crates/frontend/src/pages/ExperimentList/ExperimentList.rs rename to crates/frontend/src/pages/experiment_list/experiment_list.rs index 6cd6fb63a..de6688e88 100644 --- a/crates/frontend/src/pages/ExperimentList/ExperimentList.rs +++ b/crates/frontend/src/pages/experiment_list/experiment_list.rs @@ -5,9 +5,10 @@ use leptos::*; use chrono::{prelude::Utc, TimeZone}; use serde::{Deserialize, Serialize}; +use crate::components::drawer::drawer::{close_drawer, Drawer, DrawerBtn}; use crate::components::{ - button::button::Button, experiment_form::experiment_form::ExperimentForm, - pagination::pagination::Pagination, stat::stat::Stat, table::table::Table, + experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, + stat::stat::Stat, table::table::Table, }; use crate::types::{ExperimentsResponse, ListFilters}; @@ -18,7 +19,6 @@ use crate::{ types::{DefaultConfig, Dimension}, }; use serde_json::{json, Map, Value}; -use wasm_bindgen::JsCast; #[derive(Serialize, Deserialize, Clone, Debug)] struct CombinedResource { @@ -28,7 +28,7 @@ struct CombinedResource { } #[component] -pub fn ExperimentList() -> impl IntoView { +pub fn experiment_list() -> impl IntoView { // acquire tenant let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let (filters, set_filters) = create_signal(ListFilters { @@ -38,8 +38,8 @@ pub fn ExperimentList() -> impl IntoView { page: Some(1), count: Some(10), }); - let (open_form_modal, set_open_form_modal) = create_signal(false); + let (reset_exp_form, set_exp_form) = create_signal(0); let table_columns = create_memo(move |_| experiment_table_columns()); let combined_resource: Resource<(String, ListFilters), CombinedResource> = @@ -72,18 +72,10 @@ pub fn ExperimentList() -> impl IntoView { let handle_submit_experiment_form = move || { combined_resource.refetch(); - set_open_form_modal.set(false); - if let Some(element) = document().get_element_by_id("create_exp_modal") { - let dialog_ele = element.dyn_ref::<web_sys::HtmlDialogElement>(); - match dialog_ele { - Some(ele) => { - ele.close(); - } - None => { - log!("no modal element"); - } - } - } + set_exp_form.update(|val| { + *val += 1; + }); + close_drawer("create_exp_drawer"); }; // TODO: Add filters @@ -113,29 +105,10 @@ pub fn ExperimentList() -> impl IntoView { <div class="flex justify-between"> <h2 class="card-title">Experiments</h2> <div> - <Button - on_click=move |event: web_sys::MouseEvent| { - event.prevent_default(); - set_open_form_modal.set(true); - if let Some(element) = document() - .get_element_by_id("create_exp_modal") - { - log!("opening the experiment modal"); - let dialog_ele = element - .dyn_ref::<web_sys::HtmlDialogElement>(); - match dialog_ele { - Some(ele) => { - let _ = ele.show_modal(); - } - None => { - log!("no modal element"); - } - } - } - } - - text="Create Experiment".to_string() - /> + <DrawerBtn drawer_id="create_exp_drawer" + .to_string()> + Create Experiment <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> </div> </div> <div> @@ -249,51 +222,18 @@ pub fn ExperimentList() -> impl IntoView { default_config: vec![], }) .default_config; + let _ = reset_exp_form.get(); view! { - <Show when=move || { open_form_modal.get() }> - <dialog id="create_exp_modal" class="modal"> - <div class="modal-box w-12/12 max-w-5xl"> - <div class="flex justify-between"> - <h3 class="font-bold text-lg">Create Experiment</h3> - <div> - <button on:click=move |_| { - set_open_form_modal.set(false); - if let Some(element) = document() - .get_element_by_id("create_exp_modal") - { - log!("FOUND AND CLOSING THE FORM"); - let dialog_ele = element - .dyn_ref::<web_sys::HtmlDialogElement>(); - match dialog_ele { - Some(ele) => { - ele.close(); - } - None => { - log!("no modal element"); - } - } - } else { - log!("outer close button no modal element"); - } - }> - - <i class="ri-close-fill"></i> - </button> - </div> - </div> - <div class="modal-action flex flex-col"> - <ExperimentForm - name="".to_string() - context=vec![] - variants=vec![] - dimensions=dim.clone() - default_config=def_conf.clone() - handle_submit=handle_submit_experiment_form - /> - </div> - </div> - </dialog> - </Show> + <Drawer id="create_exp_drawer".to_string() header="Create New Experiment"> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim.clone() + default_config=def_conf.clone() + handle_submit=handle_submit_experiment_form + /> + </Drawer> } }} diff --git a/crates/frontend/src/pages/experiment_list/mod.rs b/crates/frontend/src/pages/experiment_list/mod.rs new file mode 100644 index 000000000..5fdbbd12d --- /dev/null +++ b/crates/frontend/src/pages/experiment_list/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_list; +pub mod utils; diff --git a/crates/frontend/src/pages/ExperimentList/utils.rs b/crates/frontend/src/pages/experiment_list/utils.rs similarity index 100% rename from crates/frontend/src/pages/ExperimentList/utils.rs rename to crates/frontend/src/pages/experiment_list/utils.rs diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 3e14dd908..bdb34b612 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -4,6 +4,6 @@ pub mod ContextOverride; pub mod DefaultConfig; pub mod Dimensions; pub mod Experiment; -pub mod ExperimentList; pub mod Home; pub mod NotFound; +pub mod experiment_list; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index bf53a42bb..cb9cd0290 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -6,6 +6,8 @@ use chrono::{DateTime, Utc}; use derive_more::{Deref, DerefMut}; use serde_json::{Map, Value}; +use crate::components::dropdown::utils::DropdownOption; + #[derive(Clone, Debug)] pub struct AppRoute { pub key: String, @@ -135,6 +137,15 @@ pub struct Dimension { pub schema: Value, } +impl DropdownOption for Dimension { + fn key(&self) -> String { + self.dimension.clone() + } + fn label(&self) -> String { + self.dimension.clone() + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct DefaultConfig { pub key: String, @@ -144,6 +155,15 @@ pub struct DefaultConfig { pub schema: Value, } +impl DropdownOption for DefaultConfig { + fn key(&self) -> String { + self.key.clone() + } + fn label(&self) -> String { + self.key.clone() + } +} + #[derive(Deserialize, Serialize, Clone)] pub struct Context { pub id: String, diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index e6a5cdb34..48fe188ef 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -148,7 +148,7 @@ pub fn use_service_prefix() -> String { .unwrap_or(String::new()) } -pub fn get_element_by_id<T>(id: &'static str) -> Option<T> +pub fn get_element_by_id<T>(id: &str) -> Option<T> where T: wasm_bindgen::JsCast + Clone, { @@ -160,7 +160,7 @@ where } } -pub fn show_modal(id: &'static str) { +pub fn show_modal(id: &str) { let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); if let Some(dialog_ele) = option_dialog_ele { let _ = dialog_ele.show_modal(); @@ -168,7 +168,7 @@ pub fn show_modal(id: &'static str) { } } -pub fn close_modal(id: &'static str) { +pub fn close_modal(id: &str) { let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); if let Some(dialog_ele) = option_dialog_ele { let _ = dialog_ele.close(); diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index d6bfa4fd6..624baf642 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -25,4 +25,50 @@ .value_link { text-decoration:underline; +} + +.drawer-zindex { + z-index: 99999999; +} + +.btn-purple-outline { + @apply text-purple-500; + @apply dark:text-purple-800; + @apply bg-white; + @apply hover:bg-white; + @apply ring-2; + @apply ring-purple-500; + @apply dark:ring-purple-800; +} + +.btn-purple-link { + @apply text-purple-500; + @apply dark:text-purple-800; + @apply bg-transparent; + @apply outline-none; + @apply focus:outline-none; + @apply border-0; + @apply shadow-none; + @apply hover:bg-transparent; +} + +.btn-purple { + @apply text-white; + @apply bg-gradient-to-r; + @apply from-purple-500; + @apply via-purple-600; + @apply to-purple-700; + @apply hover:bg-gradient-to-br; + @apply focus:ring-4; + @apply focus:outline-none; + @apply focus:ring-purple-300; + @apply dark:focus:ring-purple-800; + @apply shadow-lg; + @apply shadow-purple-500/50; + @apply dark:shadow-lg; + @apply dark:shadow-purple-800/80; +} + +.word-break-break { + word-break: break-word; } \ No newline at end of file From fa09a96ed49f5de88f4817ec7b6608abea79f02d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 6 Mar 2024 15:14:10 +0530 Subject: [PATCH 293/352] fix: added frontend crate to cog.toml --- cog.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cog.toml b/cog.toml index df0eea494..6b7e26b53 100644 --- a/cog.toml +++ b/cog.toml @@ -29,6 +29,7 @@ context-aware-config = { path = "crates/context-aware-config" } experimentation-platform = { path = "crates/experimentation-platform" } service-utils = { path = "crates/service-utils" } external = { path = "crates/external" } +frontend = { path = "crates/frontend" } cac_client = { path = "crates/cac_client" } superposition_client = { path = "crates/superposition_client" } \ No newline at end of file From 53a20b59c7ff6c6394857731fe7f47ab151e3320 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 6 Mar 2024 09:56:31 +0000 Subject: [PATCH 294/352] chore(version): v0.29.0 [skip ci] --- CHANGELOG.md | 11 +++++++ crates/frontend/CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 crates/frontend/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f39ecf9..e05e4d4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.29.0 - 2024-03-06 +### Package updates +- frontend bumped to frontend-v0.1.0 +### Global changes +#### Bug Fixes +- added frontend crate to cog.toml - (c901e21) - Shubhranshu Sanjeev +#### Features +- PICAF-26266 url click and text wrap fixes - (643c54d) - Saurav CV + +- - - + ## v0.28.0 - 2024-03-04 ### Package updates - cac_client bumped to cac_client-v0.6.0 diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md new file mode 100644 index 000000000..abd3e654c --- /dev/null +++ b/crates/frontend/CHANGELOG.md @@ -0,0 +1,64 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## frontend-v0.1.0 - 2024-03-06 +#### Bug Fixes +- added drawer, improved UX & single click override addition to variants - (14a1ead) - Shubhranshu Sanjeev +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik +- [PICAF-26196] add traffic percentage to experiments table - (5fb0221) - Kartik +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik +- using SERVICE_NAME in is_server instead of SERVER_NAME(wrong var name) - (efe97f0) - Shubhranshu Sanjeev +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins +- refactored experiment page and fixed experiment edit flow - (b153486) - Shubhranshu Sanjeev +- getting api hostname from env for frontend - (837899d) - Shubhranshu Sanjeev +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev +- refactored DefaultConfig component + fixed edit flow - (f2d38cc) - Shubhranshu Sanjeev +- fixed dimension form edit flow + fixed table component CellFormatter to accept move closures - (9c3a364) - Shubhranshu Sanjeev +- frontend build process - (cbdad01) - Shubhranshu Sanjeev +- fixed tenant hydration bug - (cf0e633) - Saurav Suman +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- cleanup code - (4820f31) - Kartik Gajendra +- UI fixes for demo - (4927766) - Kartik Gajendra +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +- context parsing - (d46ca42) - Kartik Gajendra +- resolve UI bugs - (98695a8) - Kartik Gajendra +- dimensions page updates - (5220b36) - ankit.mahato +#### Features +- PICAF-26266 url click and text wrap fixes - (643c54d) - Saurav CV +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev +- added bool, i64 and decimal in default config form - (fca1ca6) - Saurav Suman +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman +- added between in frontend - (0eb60e5) - Akhilesh Bhadauriya +- added validation inside default config form , formatted dates , added disable feature of edit - (cacf20f) - Saurav Suman +- resolve page with unified UI - (e84eb41) - Kartik Gajendra +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed experiment suspense block , added generic button - (117bfc8) - Saurav Suman +- experiment create form - (91371c0) - Shubhranshu Sanjeev +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working resolve page - (81c83d4) - Kartik Gajendra +- added state changes in the form - (b64a227) - Saurav Suman +- testing create form - (d0a5aea) - Kartik Gajendra +- working experiments page - (81b17dc) - Kartik Gajendra +- experiment UI - (72e19e6) - Kartik Gajendra +- added default config and override screen - (cd4267e) - Saurav Suman +- added default config page - (95e909d) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- override and context form - (553e3ad) - Shubhranshu Sanjeev +- dimensions - (c5e94fa) - ankit.mahato +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +- formatted frontend code - (70f873f) - Shubhranshu Sanjeev +#### Refactoring +- using snake case for component fxn names - (19e9aca) - Shubhranshu Sanjeev +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file From 096d68390323ce1d0f37afe1a2e4b1ed5461842d Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Fri, 10 Nov 2023 16:36:11 +0530 Subject: [PATCH 295/352] feat: added CAC language support --- Cargo.lock | 195 +++++++++++++++++++++++++++++--- Cargo.toml | 11 +- crates/caclang/Cargo.toml | 26 +++++ crates/caclang/cac_example.toml | 31 +++++ crates/caclang/mjos.toml | 63 +++++++++++ crates/caclang/src/bin.rs | 63 +++++++++++ crates/caclang/src/lib.rs | 144 +++++++++++++++++++++++ crates/caclang/src/parse.rs | 109 ++++++++++++++++++ 8 files changed, 621 insertions(+), 21 deletions(-) create mode 100644 crates/caclang/Cargo.toml create mode 100644 crates/caclang/cac_example.toml create mode 100644 crates/caclang/mjos.toml create mode 100644 crates/caclang/src/bin.rs create mode 100644 crates/caclang/src/lib.rs create mode 100644 crates/caclang/src/parse.rs diff --git a/Cargo.lock b/Cargo.lock index 33e571380..36ea571b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -657,6 +657,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" +[[package]] +name = "caclang" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "derive_more", + "evalexpr", + "inquire", + "regex", + "serde", + "strum", + "strum_macros", + "toml 0.8.8", +] + [[package]] name = "camino" version = "1.1.6" @@ -795,7 +811,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml", + "toml 0.5.11", ] [[package]] @@ -964,6 +980,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1207,6 +1248,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + [[package]] name = "either" version = "1.9.0" @@ -1284,6 +1331,12 @@ dependencies = [ "libc", ] +[[package]] +name = "evalexpr" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e757e796a66b54d19fa26de38e75c3351eb7a3755c85d7d181a8c61437ff60c" + [[package]] name = "example" version = "0.1.0" @@ -1616,9 +1669,9 @@ checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", "allocator-api2", @@ -1823,7 +1876,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.2", +] + +[[package]] +name = "inquire" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +dependencies = [ + "bitflags 1.3.2", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -2279,7 +2348,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] @@ -2311,7 +2380,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -2407,6 +2476,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nom" version = "7.1.3" @@ -2860,13 +2938,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] @@ -2878,6 +2957,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2886,9 +2976,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" @@ -3274,6 +3364,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "serde_test" version = "1.0.176" @@ -3413,6 +3512,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3481,21 +3601,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.24.3" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -3791,6 +3911,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -4450,6 +4604,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index e5f5afa65..52225a49d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,8 @@ members = [ "crates/superposition_client", "crates/cac_client", "crates/superposition_client_integration_example", - "crates/frontend" + "crates/frontend", + "crates/caclang" ] [[workspace.metadata.leptos]] @@ -44,12 +45,12 @@ bytes = "1.4.0" rusoto_core = "0.48.0" rand = "0.8.5" once_cell = { version = "1.18.0" } -anyhow = { version = "1.0", default-features = false } -strum_macros = "^0.24" -strum = {version = "^0.24"} +anyhow = "1.0.75" +strum_macros = "0.25" +strum = "0.25" # juspay dependencies dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.4"} tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.1.0" } leptos = { version = "0.5.2" } leptos_meta = { version = "0.5.2" } -leptos_router = { version = "0.5.2" } \ No newline at end of file +leptos_router = { version = "0.5.2" } diff --git a/crates/caclang/Cargo.toml b/crates/caclang/Cargo.toml new file mode 100644 index 000000000..d633990c2 --- /dev/null +++ b/crates/caclang/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "caclang" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "caclang" +path = "src/lib.rs" + +[[bin]] +name = "cac" +path = "src/bin.rs" + + +[dependencies] +toml = "0.8.8" +clap = { version = "4.3.0", features = ["derive"] } +inquire = "0.6.2" +regex = "1.9.1" +serde = "1.0.163" +anyhow = { workspace = true } +derive_more = { workspace = true } +evalexpr = "11.1.0" +strum_macros = { workspace = true } +strum = {workspace = true } diff --git a/crates/caclang/cac_example.toml b/crates/caclang/cac_example.toml new file mode 100644 index 000000000..d6830106a --- /dev/null +++ b/crates/caclang/cac_example.toml @@ -0,0 +1,31 @@ +[dimensions] + +# priority grows exponentially in powers of 2, highest priority items win +height = { type = "number" } # priority 1 +fur-pattern = { type = "string", enum = [ + "fluffy", + "straight", + "nofur", +] } # priority 2 +color = { type = "string", pattern = "a-zA-Z+" } # priority 4 +breed = { type = "string", pattern = "a-zA-Z+" } # priority 8 + +[default-configs] + +name = "dog" +owner = "someone" + +[overrides] + +override_one = { name = "rocky", owner = "Ritick" } +override_two = { name = "bobo", owner = "Shubranshu" } +override_three = { name = "blondie" } +override_four = { owner = "Natarajan" } +override_five = { owner = "Saurav" } + +[contexts] + +"breed = golden_retriever" = ["override_three", "override_five"] +"breed = labrador and height > 3" = ["override_one"] +"breed = pug and fur_pattern = straight" = ["override_two"] +"color = black" = ["override_three", "override_four"] diff --git a/crates/caclang/mjos.toml b/crates/caclang/mjos.toml new file mode 100644 index 000000000..4d91df6e1 --- /dev/null +++ b/crates/caclang/mjos.toml @@ -0,0 +1,63 @@ +[dimensions] + +# priority grows exponentially in powers of 2 starting with the first entry, highest priority items win +os = { pattern = "(android|ios|web)", type = "string", priority = 1 } # priority 1 +toss = { type = "number", priority = 2 } # priority 2 +clientId = { pattern = "^[a-z0-9].*$", type = "string", priority = 4 } # priority 4 +scope = { pattern = "(beta|release|cug)", type = "string", priority = 8 } # priority 8 +internalUser = { type = "boolean", priority = 16 } # priority 16 +tier = { type = "number", priority = 32 } # priority 32 + +[default-configs] + +android_hyperpay_version = "2.1.1_android" +android_godel_config = "1.0" +android_hyperpay_configuration = "hyperpay configuration android url" +android_hyperpay_strings = "hyperpay strings android url" +android_hyperpay_icons = "hyperpay icons android url" +android_hyperupi_version = "2.2.2_upi_android" +android_hyperupi_configuration = "hyperupi configuration android url" +android_hyperupi_strings = "hyperupi strings android url" +android_hyperupi_icons = "hyperupi icons android url" + +ios_hyperpay_version = "2.1.1_ios" +ios_hyperpay_configuration = "hyperpay configuration ios url" +ios_hyperpay_strings = "hyperpay strings ios url" +ios_hyperpay_icons = "hyperpay icons ios url" +ios_hyperupi_version = "2.2.2_upi_ios" +ios_hyperupi_configuration = "hyperupi configuration ios url" +ios_hyperupi_strings = "hyperupi strings ios url" +ios_hyperupi_icons = "hyperupi icons ios url" + +web_hyperpay_version = "2.1.1_web" +web_hyperpay_configuration = "hyperpay configuration web url" +web_hyperpay_strings = "hyperpay strings web url" +web_hyperpay_icons = "hyperpay icons web url" +web_hyperupi_version = "2.2.2_upi_web" +web_hyperupi_configuration = "hyperupi configuration web url" +web_hyperupi_strings = "hyperupi strings web url" +web_hyperupi_icons = "hyperupi icons web url" + + +["os = android"] +android_godel_config = "1.3" + +["os = android and clientId IN [meesho,zee5]"] +android_hyperupi_version = "1_android" +android_hyperupi_configuration = "PICAF-22147" +android_hyperpay_version = "1.2.1_android" +android_hyperpay_configuration = "PICAF-22145" + +["os = ios and clientId IN [a23games,galactustest]"] +ios_hyperupi_version = "10.2.2_upi_ios" + +["os IN [web,ios] and toss = -1"] +ios_hyperupi_icons = "PICAF-22146" + +["clientId = cac"] +android_hyperpay_version = "1.2.1_android" +android_hyperpay_configuration = "PICAF-22145" + +["tier IN [1,2]"] +android_hyperpay_version = "1.0.1_android" +# godel_config = "1.0" diff --git a/crates/caclang/src/bin.rs b/crates/caclang/src/bin.rs new file mode 100644 index 000000000..002a15b49 --- /dev/null +++ b/crates/caclang/src/bin.rs @@ -0,0 +1,63 @@ +use std::fs; + +use caclang::{ContextAwareConfig, DataType}; +use clap::Parser; +use evalexpr::{ContextWithMutableVariables, HashMapContext}; +use inquire::{CustomType, Text}; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct Args { + #[arg(short, long)] + file: String, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let config = fs::read_to_string(args.file)?; + let cac = ContextAwareConfig::parse(config)?; + loop { + let mut context = HashMapContext::new(); + for (dimension, props) in cac.dimensions.iter() { + if props.data_type == DataType::String { + if let Some(user_value) = + Text::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + } else if props.data_type == DataType::Number { + if let Some(user_value) = + CustomType::<i64>::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + } else { + if let Some(user_value) = + CustomType::<bool>::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + }; + } + for ctx in cac.contexts.iter() { + let eval = ctx.expr.eval_with_context(&context); + if eval.is_ok() && eval?.as_boolean()? { + println!("============================================"); + println!("context {}", ctx.context); + println!("Expression {:?}", ctx.expr.to_string()); + println!("overrides {:?}", ctx.overrides); + println!("priority {:?}", ctx.calculated_priority); + println!("============================================"); + } + } + if CustomType::<bool>::new("Hit Esc to stop, enter to continue") + .prompt_skippable()? + .is_none() + { + return Ok(()); + } + } +} diff --git a/crates/caclang/src/lib.rs b/crates/caclang/src/lib.rs new file mode 100644 index 000000000..db04fde96 --- /dev/null +++ b/crates/caclang/src/lib.rs @@ -0,0 +1,144 @@ +use std::{collections::HashMap, str::FromStr}; +mod parse; + +use anyhow::anyhow; +use derive_more::Deref; +use evalexpr::Node; +use parse::{extract_section, parse_and_validate_ctx}; +use regex::Regex; +use strum_macros::EnumString; +use toml::Table; + +#[derive(Debug, Clone, EnumString, PartialEq)] +pub enum DataType { + #[strum(ascii_case_insensitive)] + String, + #[strum(ascii_case_insensitive)] + Number, + #[strum(ascii_case_insensitive)] + Boolean, +} +#[derive(Debug, Clone)] +pub struct Context { + pub context: String, + pub expr: Node, + pub overrides: HashMap<String, String>, + pub calculated_priority: u64, +} + +impl Context { + pub fn from( + dimensions: &Dimensions, + default_config: &Table, + ctx: String, + o: &Table, + ) -> anyhow::Result<Self> { + let (expr, calculated_priority) = parse_and_validate_ctx(dimensions, &ctx)?; + let mut overrides = HashMap::new(); + for (config_key, ov) in o.into_iter() { + default_config.get(config_key).ok_or(anyhow!("The override key {config_key} for the context {ctx} is not present in default-configs section"))?; + let ove = ov.as_str().ok_or(anyhow!("Invalid syntax for override for the context {ctx} and config key {config_key}"))?.to_string(); + overrides.insert(config_key.into(), ove); + } + Ok(Self { + context: ctx, + expr, + overrides, + calculated_priority, + }) + } +} + +#[derive(Debug, Clone, Deref)] +pub struct Contexts(pub Vec<Context>); + +impl Contexts { + pub fn from( + dimensions: &Dimensions, + default_config: &Table, + value: Table, + ) -> anyhow::Result<Self> { + let mut items = Vec::new(); + for (ctx, overrides) in value.into_iter() { + let overrides = overrides + .as_table() + .ok_or(anyhow!("invalid overrides provided for {ctx}"))?; + items.push(Context::from(dimensions, default_config, ctx, overrides)?); + } + + Ok(Contexts(items)) + } + + +} + +#[derive(Debug, Clone)] +pub struct DimensionProperties { + pub data_type: DataType, + pub pattern: Regex, + pub priority: u64, +} + +impl DimensionProperties { + fn from(value: &Table) -> anyhow::Result<Self> { + let data_type = DataType::from_str( + value + .get("type") + .ok_or(anyhow!("type is missing from dimension"))? + .as_str() + .ok_or(anyhow!( + "Invalid type, allowed values are string, number and boolean" + ))?, + ) + .unwrap(); + let pattern = if let Some(p) = value.get("pattern") { + Regex::new(p.as_str().ok_or(anyhow!("Invalid regex expression"))?)? + } else { + Regex::new(".*")? + }; + let priority = value + .get("priority") + .ok_or(anyhow!("Priority field is missing"))? + .as_integer() + .ok_or(anyhow!("invalid value for priority"))? as u64; + Ok(Self { + data_type, + pattern, + priority, + }) + } +} + +#[derive(Debug, Clone, Deref)] +pub struct Dimensions(pub HashMap<String, DimensionProperties>); + +impl Dimensions { + pub fn from(value: Table) -> anyhow::Result<Self> { + let mut dimension_map = HashMap::new(); + for (dimension, props) in value.into_iter() { + let properties: &Table = props.as_table().unwrap(); + dimension_map.insert(dimension, DimensionProperties::from(properties)?); + } + Ok(Dimensions(dimension_map)) + } +} + +#[derive(Debug, Clone)] +pub struct ContextAwareConfig { + pub dimensions: Dimensions, + pub default_config: Table, + pub contexts: Contexts, +} +impl ContextAwareConfig { + pub fn parse(config: String) -> anyhow::Result<Self> { + let mut cac: Table = config.parse::<Table>()?; + let dimensions = Dimensions::from(extract_section(&mut cac, "dimensions")?)?; + let default_config = extract_section(&mut cac, "default-configs")?; + let contexts = Contexts::from(&dimensions, &default_config, cac)?; + Ok(Self { + dimensions, + default_config, + contexts, + }) + } +} diff --git a/crates/caclang/src/parse.rs b/crates/caclang/src/parse.rs new file mode 100644 index 000000000..98b85f76d --- /dev/null +++ b/crates/caclang/src/parse.rs @@ -0,0 +1,109 @@ +use anyhow::anyhow; +use evalexpr::{build_operator_tree, Node}; +use toml::Table; + +use crate::{DataType, Dimensions}; + +const AND_TOKEN: &str = " and "; +const JOIN_TOKEN: &str = " && "; + +pub(crate) fn extract_section( + item: &mut Table, + section_name: &'static str, +) -> anyhow::Result<Table> { + let section = item + .remove(section_name) + .ok_or(anyhow!("{section_name} section not found"))?; + let section = section.as_table().ok_or(anyhow!( + "The formatting of the {section_name} is incorrect. Please check the docs" + ))?; + Ok(section.clone()) +} + +pub(crate) fn parse_and_validate_ctx( + dimensions: &Dimensions, + ctx: &String, +) -> anyhow::Result<(Node, u64)> { + let mut expr_ctx: Vec<String> = Vec::new(); + let mut priority: u64 = 0; + for rule in ctx.split(AND_TOKEN).into_iter() { + let parts: Vec<&str> = rule.trim().split_whitespace().collect(); + if parts.len() != 3 { + return Err(anyhow!("Invalid rule {rule} in context {ctx}")); + } + let mut tokens = parts.into_iter(); + let (dimension, operator, value) = ( + tokens + .next() + .ok_or(anyhow!("Dimension not found in {rule}"))?, + tokens + .next() + .ok_or(anyhow!("Operator not found in {rule}"))?, + tokens.next().ok_or(anyhow!("Value not found in {rule}"))?, + ); + let props = dimensions.get(dimension).ok_or(anyhow!( + "Dimension {dimension} is not defined in dimensions section" + ))?; + + let validator = |value| -> anyhow::Result<()> { + if !props.pattern.is_match(value) { + return Err(anyhow!("Invalid value for dimension {dimension} in context {ctx}, the pattern defined does not match with the value provided")); + } + Ok(()) + }; + + let gen_expr = + |op: &str, dimension: &str, value: &str, data_type: &DataType| -> String { + match (op, data_type) { + ("IN", DataType::String) => { + format!("contains(({value}), {dimension})") + } + ("IN", _) => format!("contains(({value}),{dimension})"), + (_, DataType::String) => format!("{dimension} {op} \"{value}\""), + (_, _) => format!("{dimension} {op} {value}"), + } + }; + + let expr_rule = match operator { + "=" => { + validator(value)?; + gen_expr("==", dimension, value, &props.data_type) + } + ">=" => { + validator(value)?; + gen_expr(">=", dimension, value, &props.data_type) + } + "<=" => { + validator(value)?; + gen_expr("<=", dimension, value, &props.data_type) + } + "!=" => { + validator(value)?; + gen_expr("!=", dimension, value, &props.data_type) + } + "IN" => { + let values = &value[1..value.len() - 1]; + let mut items: Vec<String> = Vec::new(); + for item in values.split(',').into_iter() { + validator(item)?; + if props.data_type == DataType::String { + items.push(format!("\"{item}\"")); + } else { + items.push(format!("{item}")); + + } + } + gen_expr("IN", dimension, items.join(",").as_str(), &props.data_type) + } + _ => { + return Err(anyhow!( + "Unsupported operation {operator} in rule {rule} in context {ctx}" + )) + } + }; + priority += props.priority; + expr_ctx.push(expr_rule); + } + let expr = build_operator_tree(expr_ctx.join(JOIN_TOKEN).as_str())?; + Ok((expr, priority)) +} From 2c3d0dcf7b925279efa52e4fe38dcf4700aaf73d Mon Sep 17 00:00:00 2001 From: Kartik Gajendra <kartik.gajendra@juspay.in> Date: Thu, 16 Nov 2023 18:04:45 +0530 Subject: [PATCH 296/352] feat: support more operations --- Cargo.lock | 3 + Cargo.toml | 1 + cog.toml | 2 +- crates/caclang/Cargo.toml | 5 +- crates/caclang/mjos.toml | 13 +++-- crates/caclang/src/bin.rs | 35 +++++------- crates/caclang/src/{parse.rs => helpers.rs} | 55 +++++++++++++++++- crates/caclang/src/lib.rs | 63 ++++++++++++++++++--- crates/context-aware-config/Cargo.toml | 2 +- 9 files changed, 138 insertions(+), 41 deletions(-) rename crates/caclang/src/{parse.rs => helpers.rs} (66%) diff --git a/Cargo.lock b/Cargo.lock index 36ea571b0..e20969a19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,10 +662,13 @@ name = "caclang" version = "0.1.0" dependencies = [ "anyhow", + "blake3", "clap", "derive_more", + "env_logger 0.8.4", "evalexpr", "inquire", + "log", "regex", "serde", "strum", diff --git a/Cargo.toml b/Cargo.toml index 52225a49d..4f243cd64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ once_cell = { version = "1.18.0" } anyhow = "1.0.75" strum_macros = "0.25" strum = "0.25" +blake3 = "1.3.3" # juspay dependencies dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.4"} tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.1.0" } diff --git a/cog.toml b/cog.toml index 6b7e26b53..51355045e 100644 --- a/cog.toml +++ b/cog.toml @@ -30,6 +30,6 @@ experimentation-platform = { path = "crates/experimentation-platform" } service-utils = { path = "crates/service-utils" } external = { path = "crates/external" } frontend = { path = "crates/frontend" } - +caclang = { path = "crates/caclang" } cac_client = { path = "crates/cac_client" } superposition_client = { path = "crates/superposition_client" } \ No newline at end of file diff --git a/crates/caclang/Cargo.toml b/crates/caclang/Cargo.toml index d633990c2..8b4c2ef28 100644 --- a/crates/caclang/Cargo.toml +++ b/crates/caclang/Cargo.toml @@ -19,8 +19,11 @@ clap = { version = "4.3.0", features = ["derive"] } inquire = "0.6.2" regex = "1.9.1" serde = "1.0.163" +blake3 = { workspace = true } anyhow = { workspace = true } derive_more = { workspace = true } evalexpr = "11.1.0" +log = { workspace = true } +env_logger = { workspace = true } strum_macros = { workspace = true } -strum = {workspace = true } +strum = { workspace = true } diff --git a/crates/caclang/mjos.toml b/crates/caclang/mjos.toml index 4d91df6e1..7dcec7ed2 100644 --- a/crates/caclang/mjos.toml +++ b/crates/caclang/mjos.toml @@ -39,25 +39,26 @@ web_hyperupi_strings = "hyperupi strings web url" web_hyperupi_icons = "hyperupi icons web url" -["os = android"] +["os is android"] android_godel_config = "1.3" +android_hyperpay_configuration = "you should not this" -["os = android and clientId IN [meesho,zee5]"] +["os is android and clientId in [meesho,zee5]"] android_hyperupi_version = "1_android" android_hyperupi_configuration = "PICAF-22147" android_hyperpay_version = "1.2.1_android" android_hyperpay_configuration = "PICAF-22145" -["os = ios and clientId IN [a23games,galactustest]"] +["os is ios and clientId in [a23games,galactustest]"] ios_hyperupi_version = "10.2.2_upi_ios" -["os IN [web,ios] and toss = -1"] +["os in [web,ios] and toss is -1"] ios_hyperupi_icons = "PICAF-22146" -["clientId = cac"] +["clientId is cac"] android_hyperpay_version = "1.2.1_android" android_hyperpay_configuration = "PICAF-22145" -["tier IN [1,2]"] +["tier in [1,2]"] android_hyperpay_version = "1.0.1_android" # godel_config = "1.0" diff --git a/crates/caclang/src/bin.rs b/crates/caclang/src/bin.rs index 002a15b49..8b3e3e944 100644 --- a/crates/caclang/src/bin.rs +++ b/crates/caclang/src/bin.rs @@ -13,6 +13,7 @@ struct Args { } pub fn main() -> anyhow::Result<()> { + env_logger::init(); let args = Args::parse(); let config = fs::read_to_string(args.file)?; let cac = ContextAwareConfig::parse(config)?; @@ -20,39 +21,33 @@ pub fn main() -> anyhow::Result<()> { let mut context = HashMapContext::new(); for (dimension, props) in cac.dimensions.iter() { if props.data_type == DataType::String { - if let Some(user_value) = - Text::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) - .prompt_skippable()? + if let Some(user_value) = Text::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? { context.set_value(dimension.clone(), user_value.into())?; } } else if props.data_type == DataType::Number { - if let Some(user_value) = - CustomType::<i64>::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) - .prompt_skippable()? + if let Some(user_value) = CustomType::<i64>::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? { context.set_value(dimension.clone(), user_value.into())?; } } else { - if let Some(user_value) = - CustomType::<bool>::new(format!("Value for {dimension}, hit Esc key to skip").as_str()) - .prompt_skippable()? + if let Some(user_value) = CustomType::<bool>::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? { context.set_value(dimension.clone(), user_value.into())?; } }; } - for ctx in cac.contexts.iter() { - let eval = ctx.expr.eval_with_context(&context); - if eval.is_ok() && eval?.as_boolean()? { - println!("============================================"); - println!("context {}", ctx.context); - println!("Expression {:?}", ctx.expr.to_string()); - println!("overrides {:?}", ctx.overrides); - println!("priority {:?}", ctx.calculated_priority); - println!("============================================"); - } - } + let v = cac.get_config("android_hyperpay_configuration", &context)?; + println!("config value: {v}"); if CustomType::<bool>::new("Hit Esc to stop, enter to continue") .prompt_skippable()? .is_none() diff --git a/crates/caclang/src/parse.rs b/crates/caclang/src/helpers.rs similarity index 66% rename from crates/caclang/src/parse.rs rename to crates/caclang/src/helpers.rs index 98b85f76d..8684f21bc 100644 --- a/crates/caclang/src/parse.rs +++ b/crates/caclang/src/helpers.rs @@ -20,6 +20,40 @@ pub(crate) fn extract_section( Ok(section.clone()) } +pub fn compute_cac_hash(ctx: &String) -> anyhow::Result<String> { + let tokens = ctx.split(AND_TOKEN).collect::<Vec<&str>>(); + let mut final_tokens: Vec<String> = Vec::new(); + for token in tokens.into_iter() { + let mut segments = token.trim().split(" "); + let (dimension, op, value) = ( + segments + .next() + .ok_or(anyhow!("Dimension not found for {ctx} in rule {token}"))?, + segments + .next() + .ok_or(anyhow!("Operator not found for {ctx} in rule {token}"))?, + segments + .next() + .ok_or(anyhow!("Value not found for {ctx} in rule {token}"))?, + ); + if op == "IN" { + let mut list_items = + value[1..value.len() - 1].split(",").collect::<Vec<&str>>(); + list_items.sort(); + let final_list = list_items.join(","); + final_tokens.push(format!("{dimension} {op} [{final_list}]")); + } else { + final_tokens.push(String::from(token)); + } + } + final_tokens.sort(); + let to_be_hashed = final_tokens.join(",").to_string(); + log::debug!("{}", to_be_hashed); + let hash = blake3::hash(to_be_hashed.as_bytes()).to_string(); + log::debug!("hash: {}", hash); + Ok(hash) +} + pub(crate) fn parse_and_validate_ctx( dimensions: &Dimensions, ctx: &String, @@ -64,7 +98,11 @@ pub(crate) fn parse_and_validate_ctx( } }; - let expr_rule = match operator { + let expr_rule = match operator.to_lowercase().as_str() { + "is" => { + validator(value)?; + gen_expr("==", dimension, value, &props.data_type) + } "=" => { validator(value)?; gen_expr("==", dimension, value, &props.data_type) @@ -73,15 +111,27 @@ pub(crate) fn parse_and_validate_ctx( validator(value)?; gen_expr(">=", dimension, value, &props.data_type) } + ">" => { + validator(value)?; + gen_expr(">", dimension, value, &props.data_type) + } "<=" => { validator(value)?; gen_expr("<=", dimension, value, &props.data_type) } + "<" => { + validator(value)?; + gen_expr("<", dimension, value, &props.data_type) + } + "not" => { + validator(value)?; + gen_expr("!=", dimension, value, &props.data_type) + } "!=" => { validator(value)?; gen_expr("!=", dimension, value, &props.data_type) } - "IN" => { + "in" => { let values = &value[1..value.len() - 1]; let mut items: Vec<String> = Vec::new(); for item in values.split(',').into_iter() { @@ -90,7 +140,6 @@ pub(crate) fn parse_and_validate_ctx( items.push(format!("\"{item}\"")); } else { items.push(format!("{item}")); - } } gen_expr("IN", dimension, items.join(",").as_str(), &props.data_type) diff --git a/crates/caclang/src/lib.rs b/crates/caclang/src/lib.rs index db04fde96..094afd555 100644 --- a/crates/caclang/src/lib.rs +++ b/crates/caclang/src/lib.rs @@ -1,14 +1,19 @@ -use std::{collections::HashMap, str::FromStr}; -mod parse; +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; +mod helpers; use anyhow::anyhow; use derive_more::Deref; use evalexpr::Node; -use parse::{extract_section, parse_and_validate_ctx}; +use helpers::{compute_cac_hash, extract_section, parse_and_validate_ctx}; use regex::Regex; use strum_macros::EnumString; use toml::Table; +pub type HashMapContext = evalexpr::HashMapContext; + #[derive(Debug, Clone, EnumString, PartialEq)] pub enum DataType { #[strum(ascii_case_insensitive)] @@ -50,7 +55,7 @@ impl Context { } #[derive(Debug, Clone, Deref)] -pub struct Contexts(pub Vec<Context>); +pub struct Contexts(pub HashMap<String, Context>); impl Contexts { pub fn from( @@ -58,18 +63,26 @@ impl Contexts { default_config: &Table, value: Table, ) -> anyhow::Result<Self> { - let mut items = Vec::new(); + let mut items = HashMap::new(); for (ctx, overrides) in value.into_iter() { + let ctx_hash = compute_cac_hash(&ctx)?; + if items.contains_key(&ctx_hash) { + let collided_ctx: &Context = items.get(&ctx_hash).ok_or(anyhow!(""))?; + return Err(anyhow!( + "{ctx} is a logical duplicate of {}", + (*collided_ctx).context + )); + } let overrides = overrides .as_table() .ok_or(anyhow!("invalid overrides provided for {ctx}"))?; - items.push(Context::from(dimensions, default_config, ctx, overrides)?); + items.insert( + ctx_hash, + Context::from(dimensions, default_config, ctx, overrides)?, + ); } - Ok(Contexts(items)) } - - } #[derive(Debug, Clone)] @@ -141,4 +154,36 @@ impl ContextAwareConfig { contexts, }) } + + pub fn get_config( + &self, + key: &'static str, + context: &HashMapContext, + ) -> anyhow::Result<String> { + let mut sorted_map: BTreeMap<u64, &HashMap<String, String>> = BTreeMap::new(); + for (_, ctx) in self.contexts.iter() { + let eval = ctx.expr.eval_with_context(context); + if eval.is_ok() && eval?.as_boolean()? { + log::debug!("============================================"); + log::debug!("context {}", ctx.context); + log::debug!("Expression {:?}", ctx.expr.to_string()); + log::debug!("overrides {:?}", ctx.overrides); + log::debug!("priority {:?}", ctx.calculated_priority); + log::debug!("============================================"); + sorted_map.insert(ctx.calculated_priority, &ctx.overrides); + } + } + let mut fin_override: HashMap<String, String> = HashMap::new(); + for (_, ov) in sorted_map.into_iter() { + fin_override.extend(ov.to_owned()); + } + + Ok(fin_override + .get(key) + .ok_or(anyhow!( + "{key} not found in resolved contexts {:?}", + fin_override + ))? + .to_owned()) + } } diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 7b15efebf..04973fac2 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -33,7 +33,7 @@ derive_more = { workspace = true } chrono = { workspace = true } # ORM diesel = { workspace = true } -blake3 = "1.3.3" +blake3 = { workspace = true} rusoto_kms = { workspace = true } rusoto_signature = { workspace = true } bytes = { workspace = true } From 038c8981dbef9738fb33a2ce3d577e6caa0a88db Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 6 Mar 2024 10:55:32 +0000 Subject: [PATCH 297/352] chore(version): v0.30.0 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/caclang/CHANGELOG.md | 12 ++++++++++++ crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 crates/caclang/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e05e4d4a6..3106b2267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.30.0 - 2024-03-06 +### Package updates +- context-aware-config bumped to context-aware-config-v0.22.0 +- caclang bumped to caclang-v0.1.0 +### Global changes +#### Features +- support more operations - (4db2c31) - Kartik Gajendra +- added CAC language support - (c549384) - Kartik Gajendra + +- - - + ## v0.29.0 - 2024-03-06 ### Package updates - frontend bumped to frontend-v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index e20969a19..e1b5f3687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.21.0" +version = "0.22.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/caclang/CHANGELOG.md b/crates/caclang/CHANGELOG.md new file mode 100644 index 000000000..ec3704f90 --- /dev/null +++ b/crates/caclang/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## caclang-v0.1.0 - 2024-03-06 +#### Features +- support more operations - (4db2c31) - Kartik Gajendra +- added CAC language support - (c549384) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 0ece23649..8453f4803 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.22.0 - 2024-03-06 +#### Features +- support more operations - (4db2c31) - Kartik Gajendra + +- - - + ## context-aware-config-v0.21.0 - 2024-03-04 #### Features - PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 04973fac2..9e6445801 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.21.0" +version = "0.22.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 6f7018662c066ce00f60b90f1b201e0a1bd7e1bd Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Thu, 7 Mar 2024 14:55:44 +0530 Subject: [PATCH 298/352] fix: adding min-width settings for table component --- crates/frontend/src/components/experiment/experiment.rs | 2 +- crates/frontend/src/components/table/table.rs | 4 ++-- crates/frontend/src/pages/ContextOverride/ContextOverride.rs | 2 +- crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs | 2 +- crates/frontend/src/pages/Dimensions/Dimensions.rs | 2 +- crates/frontend/src/pages/experiment_list/experiment_list.rs | 2 +- crates/frontend/styles/tailwind.css | 4 ++++ 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index 79d44f0e6..bae58b504 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -193,7 +193,7 @@ where let (rows, columns) = gen_variant_table(&exp.variants).unwrap(); view! { <Table - table_style="abc".to_string() + cell_style="min-w-48 font-mono".to_string() rows=rows key_column="overrides".to_string() columns=columns diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs index bc963d76b..d6143e215 100644 --- a/crates/frontend/src/components/table/table.rs +++ b/crates/frontend/src/components/table/table.rs @@ -20,7 +20,7 @@ fn generate_table_row_str(row: &Value) -> String { #[component] pub fn table( key_column: String, - table_style: String, + cell_style: String, columns: Vec<Column>, rows: Vec<Map<String, Value>>, ) -> impl IntoView { @@ -68,7 +68,7 @@ pub fn table( row.get(cname).unwrap_or(&Value::String("".to_string())), ); view! { - <td class=table_style + <td class=cell_style .to_string()>{(column.formatter)(&value, &row)}</td> } }) diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs index 86e7e4c56..2abaa9f27 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/ContextOverride.rs @@ -304,7 +304,7 @@ pub fn ContextOverride() -> impl IntoView { </div> <div class="space-x-4"> <Table - table_style="font-mono".to_string() + cell_style="min-w-48 font-mono".to_string() rows=contexts.clone() key_column="id".to_string() columns=table_columns.get() diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 8f77cf907..47e8a1f79 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -205,7 +205,7 @@ pub fn DefaultConfig() -> impl IntoView { </div> <Table - table_style="font-mono".to_string() + cell_style="min-w-48 font-mono".to_string() rows=table_rows key_column="id".to_string() columns=table_columns.get() diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index 0662f15d8..da89d7eef 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -217,7 +217,7 @@ pub fn Dimensions() -> impl IntoView { .to_owned(); view! { <Table - table_style="abc".to_string() + cell_style="min-w-48 font-mono".to_string() rows=data key_column="id".to_string() columns=table_columns.get() diff --git a/crates/frontend/src/pages/experiment_list/experiment_list.rs b/crates/frontend/src/pages/experiment_list/experiment_list.rs index de6688e88..4f16411da 100644 --- a/crates/frontend/src/pages/experiment_list/experiment_list.rs +++ b/crates/frontend/src/pages/experiment_list/experiment_list.rs @@ -142,7 +142,7 @@ pub fn experiment_list() -> impl IntoView { .to_owned(); view! { <Table - table_style="abc".to_string() + cell_style="min-w-48 font-mono".to_string() rows=data key_column="id".to_string() columns=table_columns.get() diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css index 624baf642..63f6b31de 100644 --- a/crates/frontend/styles/tailwind.css +++ b/crates/frontend/styles/tailwind.css @@ -71,4 +71,8 @@ .word-break-break { word-break: break-word; +} + +.min-w-48 { + min-width: 12rem; } \ No newline at end of file From fd38192a0be9a1134a6e1d88e9557f5f2d1d61be Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 7 Mar 2024 13:12:12 +0000 Subject: [PATCH 299/352] chore(version): v0.30.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/frontend/CHANGELOG.md | 6 ++++++ crates/frontend/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3106b2267..5c9059c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.30.1 - 2024-03-07 +### Package updates +- frontend bumped to frontend-v0.1.1 +### Global changes + +- - - + ## v0.30.0 - 2024-03-06 ### Package updates - context-aware-config bumped to context-aware-config-v0.22.0 diff --git a/Cargo.lock b/Cargo.lock index e1b5f3687..4ddc04c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.1.0" +version = "0.1.1" dependencies = [ "actix-files", "actix-web", diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index abd3e654c..7dd68d08e 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.1.1 - 2024-03-07 +#### Bug Fixes +- adding min-width settings for table component - (0fcd0c1) - Kartik + +- - - + ## frontend-v0.1.0 - 2024-03-06 #### Bug Fixes - added drawer, improved UX & single click override addition to variants - (14a1ead) - Shubhranshu Sanjeev diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 651dbb4de..7fbc7c4cf 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.1.0" +version = "0.1.1" edition = "2021" [lib] From b3ad9590ee8ef78eedf6e38a1563ae747b585a86 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Mon, 4 Mar 2024 16:00:41 +0530 Subject: [PATCH 300/352] feat: Added function validation for context and default_config --- .../down.sql | 1 + .../up.sql | 8 ++ .../src/api/config/handlers.rs | 5 +- .../src/api/context/handlers.rs | 16 ++- .../src/api/context/helpers.rs | 126 ++++++++++++++++++ .../src/api/context/mod.rs | 1 + .../src/api/context/types.rs | 6 + .../src/api/default_config/handlers.rs | 99 +++++++++----- .../src/api/default_config/types.rs | 1 + .../src/api/dimension/handlers.rs | 12 +- .../src/api/dimension/types.rs | 1 + .../src/api/functions/handlers.rs | 11 +- .../src/api/functions/helpers.rs | 25 ++++ crates/context-aware-config/src/db/models.rs | 4 +- crates/context-aware-config/src/db/schema.rs | 5 + .../src/validation_functions.rs | 18 ++- .../context-aware-config/tests/cac_tests.rs | 6 +- .../src/api/experiments/helpers.rs | 83 +----------- .../tests/experimentation_tests.rs | 11 +- .../external/src/api/external_api/handlers.rs | 2 +- crates/service-utils/src/helpers.rs | 84 ++++++++++++ 21 files changed, 384 insertions(+), 141 deletions(-) create mode 100644 crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql create mode 100644 crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql create mode 100644 crates/context-aware-config/src/api/context/helpers.rs diff --git a/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql b/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql b/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql new file mode 100644 index 000000000..6e74d4c11 --- /dev/null +++ b/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +ALTER TABLE public.dimensions ADD COLUMN function_name text NULL; + +ALTER TABLE public.dimensions ADD FOREIGN KEY(function_name) REFERENCES public.functions(function_name); + +ALTER TABLE public.default_configs ADD COLUMN function_name text NULL; + +ALTER TABLE public.default_configs ADD FOREIGN KEY(function_name) REFERENCES public.functions(function_name); \ No newline at end of file diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 6bb7efbd2..3bd16e04a 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -15,10 +15,11 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use experimentation_platform::api::experiments::helpers::extract_dimensions; use serde_json::{json, Map, Value, Value::Null}; use service_utils::{ - errors::types::Error as err, helpers::ToActixErr, service::types::DbConnection, + errors::types::Error as err, + helpers::{extract_dimensions, ToActixErr}, + service::types::DbConnection, types as app, }; diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index a17bc9632..3cfec2d4e 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -1,3 +1,6 @@ +extern crate base64; +use std::str; + use crate::helpers::{json_to_sorted_string, validate_context_jsonschema}; use crate::{ api::{ @@ -36,6 +39,10 @@ use serde_json::{from_value, json, Map, Value}; use service_utils::{helpers::ToActixErr, service::types::DbConnection}; use std::collections::HashMap; +use super::helpers::{ + validate_condition_with_functions, validate_override_with_functions, +}; + pub fn endpoints() -> Scope { Scope::new("") .service(put_handler) @@ -169,6 +176,8 @@ fn create_ctx_from_put_req( let ctx_condition = Value::Object(req.context.to_owned()); let ctx_override: Value = req.r#override.to_owned().into(); validate_override_with_default_configs(conn, &req.r#override)?; + validate_condition_with_functions(conn, &ctx_condition)?; + validate_override_with_functions(conn, &req.r#override)?; let dimension_schema_map = get_all_dimension_schema_map(conn)?; @@ -277,8 +286,11 @@ async fn put_handler( if let Some(io_error) = e.downcast_ref::<std::io::Error>() { log::info!("{}", { io_error }); ErrorInternalServerError("") - } else if e.to_string().contains("Bad schema") { - ErrorBadRequest("") + } else if ["Bad schema", "validation failed"] + .iter() + .any(|&s| e.to_string().contains(s)) + { + ErrorBadRequest(format!("{e}")) } else { ErrorInternalServerError("") } diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context-aware-config/src/api/context/helpers.rs new file mode 100644 index 000000000..327eb4ea4 --- /dev/null +++ b/crates/context-aware-config/src/api/context/helpers.rs @@ -0,0 +1,126 @@ +extern crate base64; +use base64::prelude::*; +use service_utils::helpers::extract_dimensions; +use std::str; + +use crate::api::functions::helpers::get_published_functions_by_names; +use crate::validation_functions::execute_fn; +use crate::{ + api::context::types::FunctionsInfo, + db::schema::{ + default_configs::dsl, + dimensions::{self}, + }, +}; +use anyhow::anyhow; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; +type DBConnection = PooledConnection<ConnectionManager<PgConnection>>; + +pub fn validate_condition_with_functions( + conn: &mut DBConnection, + context: &Value, +) -> anyhow::Result<()> { + use dimensions::dsl; + let context = extract_dimensions(&context)?; + let dimensions_list: Vec<String> = context.keys().cloned().collect(); + let keys_function_array: Vec<(String, Option<String>)> = dsl::dimensions + .filter(dsl::dimension.eq_any(dimensions_list)) + .select((dsl::dimension, dsl::function_name)) + .load(conn)?; + let new_keys_function_array: Vec<(String, String)> = keys_function_array + .into_iter() + .filter_map(|(key_, f_name)| f_name.map(|func| (key_, func))) + .collect(); + + let dimension_functions_map = get_functions_map(conn, new_keys_function_array)?; + for (key, value) in context.iter() { + if let Some(functions_map) = dimension_functions_map.get(key) { + if let (function_name, Some(function_code)) = + (functions_map.name.clone(), functions_map.code.clone()) + { + validate_value_with_function(&function_name, &function_code, key, value)?; + } + } + } + Ok(()) +} + +pub fn validate_override_with_functions( + conn: &mut DBConnection, + override_: &Map<String, Value>, +) -> anyhow::Result<()> { + let default_config_keys: Vec<String> = override_.keys().cloned().collect(); + let keys_function_array: Vec<(String, Option<String>)> = dsl::default_configs + .filter(dsl::key.eq_any(default_config_keys)) + .select((dsl::key, dsl::function_name)) + .load(conn)?; + let new_keys_function_array: Vec<(String, String)> = keys_function_array + .into_iter() + .filter_map(|(key_, f_name)| f_name.map(|func| (key_, func))) + .collect(); + + let default_config_functions_map = get_functions_map(conn, new_keys_function_array)?; + for (key, value) in override_.iter() { + if let Some(functions_map) = default_config_functions_map.get(key) { + if let (function_name, Some(function_code)) = + (functions_map.name.clone(), functions_map.code.clone()) + { + validate_value_with_function(&function_name, &function_code, key, value)?; + } + } + } + Ok(()) +} + +fn get_functions_map( + conn: &mut DBConnection, + keys_function_array: Vec<(String, String)>, +) -> anyhow::Result<HashMap<String, FunctionsInfo>> { + let functions_map: HashMap<String, Option<String>> = + get_published_functions_by_names( + conn, + keys_function_array + .iter() + .map(|(_, f_name)| f_name.clone()) + .collect(), + )? + .into_iter() + .collect(); + + let default_config_functions_map: HashMap<String, FunctionsInfo> = + keys_function_array + .into_iter() + .map(|(key, function_name)| { + ( + key.clone(), + FunctionsInfo { + name: function_name.clone(), + code: functions_map.get(&function_name).cloned().flatten(), + }, + ) + }) + .collect(); + Ok(default_config_functions_map) +} + +fn validate_value_with_function( + fun_name: &str, + function: &str, + key: &String, + value: &Value, +) -> anyhow::Result<()> { + let base64_decoded = BASE64_STANDARD.decode(function.clone())?; + let utf8_decoded = str::from_utf8(&base64_decoded)?; + if let Err((e, stdout)) = execute_fn(&utf8_decoded, fun_name, value.to_owned()) { + log::error!("function validation failed for {key} with error: {e}"); + return Err(anyhow!(json!({ + "message": format!("function validation failed for {}", key), "stdout": stdout + }))); + } + Ok(()) +} diff --git a/crates/context-aware-config/src/api/context/mod.rs b/crates/context-aware-config/src/api/context/mod.rs index ebe17b924..fa13772fb 100644 --- a/crates/context-aware-config/src/api/context/mod.rs +++ b/crates/context-aware-config/src/api/context/mod.rs @@ -1,3 +1,4 @@ mod handlers; +mod helpers; mod types; pub use handlers::endpoints; diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index d9097b02e..f8a3d5184 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -55,3 +55,9 @@ impl From<diesel::result::Error> for TransactionError { TransactionError::DieselError(error) } } + +#[derive(Deserialize, Clone)] +pub struct FunctionsInfo { + pub name: String, + pub code: Option<String>, +} diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 0b88dcd6f..54fc05b98 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,5 +1,11 @@ +extern crate base64; use super::types::CreateReq; +use base64::prelude::*; +use std::str; + +use crate::validation_functions::execute_fn; use crate::{ + api::functions::helpers::get_published_function_code, db::{self, models::DefaultConfig, schema::default_configs::dsl::default_configs}, helpers::validate_jsonschema, }; @@ -38,33 +44,44 @@ async fn create( let req = request.into_inner(); let key = key.into_inner(); - let (value, schema) = match (req.value, req.schema) { - (Some(val), Some(schema)) => (val, Value::Object(schema)), - (Some(val), None) => { - let (_, schema) = fetch_default_key(&key, &mut conn) - .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; - (val, schema) - } - (None, Some(schema)) => { - let (value, _) = fetch_default_key(&key, &mut conn) - .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; - (value, Value::Object(schema)) + if req.value.is_none() && req.schema.is_none() && req.function_name.is_none() { + log::error!("No data provided in the request body for {key}"); + return Err(ErrorBadRequest(json!({ + "message": "Please provide data in the request body." + }))); + } + + let default_config = if req.value.is_none() || req.schema.is_none() { + let (value, schema, function_name) = fetch_default_key(&key, &mut conn) + .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; + DefaultConfig { + key: key.to_owned(), + value: req.value.unwrap_or_else(|| value), + schema: req.schema.map_or_else(|| schema, Value::Object), + function_name: req.function_name.or(function_name), + created_by: user.email, + created_at: Utc::now(), } - (None, None) => { - log::info!("value/schema not provided."); - return Err(ErrorBadRequest( - json!({"message": "Either value/schema required."}), - )); + } else { + DefaultConfig { + key: key.to_owned(), + value: req.value.unwrap(), + schema: Value::Object(req.schema.unwrap()), + function_name: req.function_name, + created_by: user.email, + created_at: Utc::now(), } }; - if let Err(e) = validate_jsonschema(&state.default_config_validation_schema, &schema) - { + if let Err(e) = validate_jsonschema( + &state.default_config_validation_schema, + &default_config.schema, + ) { return Err(ErrorBadRequest(json!({ "message": e }))); }; let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) - .compile(&schema); + .compile(&default_config.schema); let jschema = match schema_compile_result { Ok(jschema) => jschema, Err(e) => { @@ -73,7 +90,7 @@ async fn create( } }; - if let Err(e) = jschema.validate(&value) { + if let Err(e) = jschema.validate(&default_config.value) { let verrors = e.collect::<Vec<ValidationError>>(); log::info!( "Validation for value with given JSON schema failed: {:?}", @@ -84,19 +101,36 @@ async fn create( )); } - let new_default_config = DefaultConfig { - key, - value, - schema, - created_by: user.email, - created_at: Utc::now(), - }; + if let Some(f_name) = &default_config.function_name { + let function_code = get_published_function_code(&mut conn, f_name.to_string()) + .map_err(|e| { + log::info!("Function not found with error : {e}"); + ErrorBadRequest(json!({"message" : "Function not found."})) + })?; + if let Some(f_code) = function_code { + let base64_decoded = BASE64_STANDARD.decode(f_code.clone()).map_err(|e| { + ErrorInternalServerError(json!({"message": e.to_string()})) + })?; + let utf8_decoded = str::from_utf8(&base64_decoded).map_err(|e| { + ErrorInternalServerError(json!({"message": e.to_string()})) + })?; + + if let Err((e, stdout)) = + execute_fn(&utf8_decoded, &f_name, default_config.value.to_owned()) + { + log::info!("function validation failed for {key} with error: {e}"); + return Err(ErrorBadRequest(json!({ + "message": "function validation failed", "stdout": stdout + }))); + } + } + } let upsert = diesel::insert_into(default_configs) - .values(&new_default_config) + .values(&default_config) .on_conflict(db::schema::default_configs::key) .do_update() - .set(&new_default_config) + .set(&default_config) .execute(&mut conn); match upsert { @@ -115,14 +149,15 @@ async fn create( fn fetch_default_key( key: &String, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> anyhow::Result<(Value, Value)> { - let res: (Value, Value) = default_configs +) -> anyhow::Result<(Value, Value, Option<String>)> { + let res: (Value, Value, Option<String>) = default_configs .filter(db::schema::default_configs::key.eq(key)) .select(( db::schema::default_configs::value, db::schema::default_configs::schema, + db::schema::default_configs::function_name, )) - .get_result::<(Value, Value)>(conn)?; + .get_result::<(Value, Value, Option<String>)>(conn)?; Ok(res) } diff --git a/crates/context-aware-config/src/api/default_config/types.rs b/crates/context-aware-config/src/api/default_config/types.rs index 559e41968..b6a53528d 100644 --- a/crates/context-aware-config/src/api/default_config/types.rs +++ b/crates/context-aware-config/src/api/default_config/types.rs @@ -6,6 +6,7 @@ pub struct CreateReq { #[serde(default, deserialize_with = "deserialize_option")] pub value: Option<Value>, pub schema: Option<Map<String, Value>>, + pub function_name: Option<String>, } fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index bed885220..e6b04fd4b 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -1,5 +1,5 @@ use crate::{ - api::dimension::types::CreateReq, + api::{dimension::types::CreateReq, functions::helpers::fetch_function}, db::{models::Dimension, schema::dimensions::dsl::*}, helpers::validate_jsonschema, }; @@ -51,12 +51,22 @@ async fn create( .body(String::from(format!("Bad schema: {:?}", e))); }; + if let Some(func_name) = create_req.function_name.clone() { + let function = fetch_function(&func_name, &mut conn); + if let Err(e) = function { + log::error!("{func_name} function not found with error: {e:?}"); + return HttpResponse::BadRequest() + .body(String::from(format!("{func_name} function not found"))); + } + } + let new_dimension = Dimension { dimension: create_req.dimension, priority: i32::from(create_req.priority), schema: schema_value, created_by: user.email, created_at: Utc::now(), + function_name: create_req.function_name, }; let upsert = diesel::insert_into(dimensions) diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs index 9b699498c..572bf21ff 100644 --- a/crates/context-aware-config/src/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -6,4 +6,5 @@ pub struct CreateReq { pub dimension: String, pub priority: u16, pub schema: Value, + pub function_name: Option<String>, } diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index 4dfc4500d..ce8e901d2 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -265,15 +265,18 @@ async fn test( Some(code) => execute_fn(&code, fun_name, req), None => { log::error!("Function test failed: function not published yet"); - Err("Function test failed as function not published yet".to_owned()) + Err(( + "Function test failed as function not published yet".to_owned(), + None, + )) } }, }; match result { - Ok(()) => Ok(HttpResponse::Ok() - .json(json!({"message": "Function validated the given value successfully"}))), - Err(e) => Err(ErrorBadRequest(json!({ "message": e }))), + Ok(stdout) => Ok(HttpResponse::Ok() + .json(json!({"message": "Function validated the given value successfully", "stdout": stdout}))), + Err((e, stdout)) => Err(ErrorBadRequest(json!({ "message": format!( "Function validation failed with error: {e}" ), "stdout": stdout }))), } } diff --git a/crates/context-aware-config/src/api/functions/helpers.rs b/crates/context-aware-config/src/api/functions/helpers.rs index 52350363c..0ecc38dd0 100644 --- a/crates/context-aware-config/src/api/functions/helpers.rs +++ b/crates/context-aware-config/src/api/functions/helpers.rs @@ -46,3 +46,28 @@ pub fn decode_base64_to_string(code: &String) -> actix_web::Result<String> { }) }) } + +pub fn get_published_function_code( + conn: &mut PooledConnection<ConnectionManager<PgConnection>>, + f_name: String, +) -> anyhow::Result<Option<String>> { + let function = functions + .filter(db::schema::functions::function_name.eq(f_name)) + .select(db::schema::functions::published_code) + .first(conn)?; + Ok(function) +} + +pub fn get_published_functions_by_names( + conn: &mut PooledConnection<ConnectionManager<PgConnection>>, + function_names: Vec<String>, +) -> anyhow::Result<Vec<(String, Option<String>)>> { + let function: Vec<(String, Option<String>)> = functions + .filter(db::schema::functions::function_name.eq_any(function_names)) + .select(( + db::schema::functions::function_name, + db::schema::functions::published_code, + )) + .load(conn)?; + Ok(function) +} diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index 9d7c1f7f2..b4362c348 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -27,9 +27,10 @@ pub struct Dimension { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, + pub function_name: Option<String>, } -#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize)] +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(key))] pub struct DefaultConfig { @@ -38,6 +39,7 @@ pub struct DefaultConfig { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, + pub function_name: Option<String>, } #[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone, Debug)] diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context-aware-config/src/db/schema.rs index 4a842675b..99d135ac1 100644 --- a/crates/context-aware-config/src/db/schema.rs +++ b/crates/context-aware-config/src/db/schema.rs @@ -20,6 +20,7 @@ diesel::table! { created_at -> Timestamptz, created_by -> Varchar, schema -> Json, + function_name -> Nullable<Text>, } } @@ -30,6 +31,7 @@ diesel::table! { created_at -> Timestamptz, created_by -> Varchar, schema -> Json, + function_name -> Nullable<Text>, } } @@ -596,6 +598,9 @@ diesel::table! { } } +diesel::joinable!(default_configs -> functions (function_name)); +diesel::joinable!(dimensions -> functions (function_name)); + diesel::allow_tables_to_appear_in_same_query!( contexts, default_configs, diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 9e53ba111..1e784fe51 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -10,6 +10,7 @@ const IMPORT_CODE: &str = r#" const EXIT_LOGIC_CODE: &str = r#" if (fun_value != true) { + console.error(fun_value) process.exit(1); }; "#; @@ -45,8 +46,8 @@ const ES_LINT_CODE: &str = r#" process.exit(1); } }).catch((error) => { + console.error(error); process.exit(1); - }); "#; @@ -56,7 +57,11 @@ fn runtime_wrapper(function_name: &str, value: Value) -> String { fun_call + EXIT_LOGIC_CODE } -pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), String> { +pub fn execute_fn( + code_str: &str, + fun_name: &str, + value: Value, +) -> Result<String, (String, Option<String>)> { let output = Command::new("node") .arg("-e") .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) @@ -64,6 +69,9 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St log::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { + let stdout = str::from_utf8(&val.stdout) + .unwrap_or("[Invalid UTF-8 in stdout]") + .to_owned(); if !(val.status.success()) { let stderr = str::from_utf8(&val.stderr) .unwrap_or("[Invalid UTF-8 in stderr]") @@ -72,14 +80,14 @@ pub fn execute_fn(code_str: &str, fun_name: &str, value: Value) -> Result<(), St "{}", format!("validation function output error: {:?}", stderr) ); - Err(stderr) + Err((stderr, Some(stdout))) } else { - Ok(()) + Ok(stdout) } } Err(e) => { log::error!("js_eval error: {}", e); - Err(format!("js_eval error: {}", e)) + Err((format!("js_eval error: {}", e), None)) } } } diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index 192a942a6..eeab38e4f 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -26,8 +26,8 @@ fn test_execute_fn() { &"test_fun".to_owned(), json!(10), ) { - Ok(()) => false, - Err(e) => e.contains("Bad schema"), + Ok(_) => false, + Err((e, stdout)) => e.contains("Bad schema"), }; let err_compile = match compile_fn(&(compile_code_error.to_owned())) { Ok(()) => false, @@ -35,7 +35,7 @@ fn test_execute_fn() { }; assert_eq!( execute_fn(&(code_ok.to_owned()), &"test_fun".to_owned(), json!(10)), - Ok(()) + Ok("true".to_string()) ); assert_eq!(err_execute, true); assert_eq!(compile_fn(&(code_ok.to_owned())), Ok(())); diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index a85939ea7..b918c2e0e 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -4,6 +4,7 @@ use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value}; use service_utils::errors::types::ErrorResponse; +use service_utils::helpers::extract_dimensions; use service_utils::service::types::ExperimentationFlags; use service_utils::{errors::types::Error as err, types as app}; use std::collections::HashSet; @@ -52,88 +53,6 @@ pub fn validate_override_keys(override_keys: &Vec<String>) -> app::Result<()> { Ok(()) } -pub fn get_variable_name_and_value(operands: &Vec<Value>) -> app::Result<(&str, &Value)> { - let (obj_pos, variable_obj) = operands - .iter() - .enumerate() - .find(|(_, operand)| { - operand.is_object() && operand.as_object().unwrap().get("var").is_some() - }) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable name from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; - - let variable_name = variable_obj - .as_object() - .map_or(None, |obj| obj.get("var")) - .map_or(None, |value| value.as_str()) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable name from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; - - let value_pos = (obj_pos + 1) % 2; - let variable_value = - operands - .get(value_pos) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable value from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; - - Ok((variable_name, variable_value)) -} - -pub fn extract_dimensions(context_json: &Value) -> app::Result<Map<String, Value>> { - // Assuming max 2-level nesting in context json logic - let context = context_json - .as_object() - .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: context not a valid JSON object".to_string(), possible_fix: "send a valid JSON context".to_string() }))?; - - let conditions = match context.get("and") { - Some(conditions_json) => conditions_json - .as_array() - .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: failed parsing conditions as an array".to_string(), possible_fix: "ensure the context provided obeys the rules of JSON logic".to_string() }))? - .clone(), - None => vec![context_json.clone()], - }; - - let mut dimension_tuples = Vec::new(); - for condition in &conditions { - let condition_obj = - condition - .as_object() - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to parse condition as an object".to_string(), - possible_fix: - "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; - let operators = condition_obj.keys(); - - for operator in operators { - let operands = condition_obj[operator].as_array().ok_or(err::BadArgument( - ErrorResponse { - message: " failed to parse operands as an arrays".to_string(), - possible_fix: - "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }, - ))?; - - let (variable_name, variable_value) = get_variable_name_and_value(operands)?; - - dimension_tuples.push((String::from(variable_name), variable_value.clone())); - } - } - - Ok(Map::from_iter(dimension_tuples)) -} - pub fn are_overlapping_contexts( context_a: &Value, context_b: &Value, diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs index 560af3031..a93c72d94 100644 --- a/crates/experimentation-platform/tests/experimentation_tests.rs +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -3,6 +3,7 @@ use experimentation_platform::api::experiments::helpers; use experimentation_platform::db::models::{Experiment, ExperimentStatusType}; use serde_json::{json, Map, Value}; use service_utils::errors::types::Error as AppError; +use service_utils::helpers::extract_dimensions; use service_utils::service::types::ExperimentationFlags; enum Dimensions { @@ -94,15 +95,9 @@ fn test_extract_dimensions() -> Result<(), AppError> { serde_json::Map::from_iter(vec![("clientId".to_string(), json!("testclient1"))]); // more than one dimension in context - assert_eq!( - helpers::extract_dimensions(&context_a)?, - expected_dimensions_1 - ); + assert_eq!(extract_dimensions(&context_a)?, expected_dimensions_1); // only one dimension in context - assert_eq!( - helpers::extract_dimensions(&context_b)?, - expected_dimensions_2 - ); + assert_eq!(extract_dimensions(&context_b)?, expected_dimensions_2); Ok(()) } diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index 19fd4bc69..d89f1f259 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -12,13 +12,13 @@ use crate::api::external_api::{ use experimentation_platform::{ api::experiments::{ handlers::{conclude, get_experiment}, - helpers::extract_dimensions, types::{ConcludeExperimentRequest, ExperimentResponse, VariantType}, }, db::models::Experiment, }; use serde_json::Value; use service_utils::{ + helpers::extract_dimensions, service::types::{AppState, DbConnection, Tenant}, types as app, }; diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 04fc1af53..8d2080305 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -8,6 +8,8 @@ use std::{ }; use super::errors::types::{Error as err, ErrorResponse}; +use crate::types as app; +use serde_json::{Map, Value}; //WARN Do NOT use this fxn inside api requests, instead add the required //env to AppState and get value from there. As this panics, it should @@ -133,3 +135,85 @@ pub fn remove_error_abstraction(e: reqwest::Error) -> err { _ => err::InternalServerErr(e.to_string()), } } + +pub fn extract_dimensions(context_json: &Value) -> app::Result<Map<String, Value>> { + // Assuming max 2-level nesting in context json logic + let context = context_json + .as_object() + .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: context not a valid JSON object".to_string(), possible_fix: "send a valid JSON context".to_string() }))?; + + let conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: failed parsing conditions as an array".to_string(), possible_fix: "ensure the context provided obeys the rules of JSON logic".to_string() }))? + .clone(), + None => vec![context_json.clone()], + }; + + let mut dimension_tuples = Vec::new(); + for condition in &conditions { + let condition_obj = + condition + .as_object() + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to parse condition as an object".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + let operators = condition_obj.keys(); + + for operator in operators { + let operands = condition_obj[operator].as_array().ok_or(err::BadArgument( + ErrorResponse { + message: " failed to parse operands as an arrays".to_string(), + possible_fix: + "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }, + ))?; + + let (variable_name, variable_value) = get_variable_name_and_value(operands)?; + + dimension_tuples.push((String::from(variable_name), variable_value.clone())); + } + } + + Ok(Map::from_iter(dimension_tuples)) +} + +pub fn get_variable_name_and_value(operands: &Vec<Value>) -> app::Result<(&str, &Value)> { + let (obj_pos, variable_obj) = operands + .iter() + .enumerate() + .find(|(_, operand)| { + operand.is_object() && operand.as_object().unwrap().get("var").is_some() + }) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable name from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + let variable_name = variable_obj + .as_object() + .map_or(None, |obj| obj.get("var")) + .map_or(None, |value| value.as_str()) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable name from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + let value_pos = (obj_pos + 1) % 2; + let variable_value = + operands + .get(value_pos) + .ok_or(err::BadArgument(ErrorResponse { + message: " failed to get variable value from operands list".to_string(), + possible_fix: "ensure the context provided obeys the rules of JSON logic" + .to_string(), + }))?; + + Ok((variable_name, variable_value)) +} From c602b93e9c35e4b38e21a4042632e96603d933d4 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Fri, 8 Mar 2024 06:36:46 +0000 Subject: [PATCH 301/352] chore(version): v0.31.0 [skip ci] --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 8 ++++---- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 6 ++++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/external/CHANGELOG.md | 6 ++++++ crates/external/Cargo.toml | 2 +- crates/service-utils/CHANGELOG.md | 6 ++++++ crates/service-utils/Cargo.toml | 2 +- 10 files changed, 42 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9059c9c..5fc1cab38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.31.0 - 2024-03-08 +### Package updates +- external bumped to external-v0.4.0 +- experimentation-platform bumped to experimentation-platform-v0.10.0 +- service-utils bumped to service-utils-v0.12.0 +- context-aware-config bumped to context-aware-config-v0.23.0 +### Global changes + +- - - + ## v0.30.1 - 2024-03-07 ### Package updates - frontend bumped to frontend-v0.1.1 diff --git a/Cargo.lock b/Cargo.lock index 4ddc04c18..9b4eeb40d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.22.0" +version = "0.23.0" dependencies = [ "actix", "actix-cors", @@ -1353,7 +1353,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.9.4" +version = "0.10.0" dependencies = [ "actix", "actix-web", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "external" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix", "actix-web", @@ -3449,7 +3449,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.11.0" +version = "0.12.0" dependencies = [ "actix", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 8453f4803..267d34033 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.23.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + ## context-aware-config-v0.22.0 - 2024-03-06 #### Features - support more operations - (4db2c31) - Kartik Gajendra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 9e6445801..b7deae705 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.22.0" +version = "0.23.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index 0f342e648..f9e2f14c2 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.10.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + ## experimentation-platform-v0.9.4 - 2024-02-27 #### Bug Fixes - returning error response if CAC call not 200 - (fa0eb5e) - Shubhranshu Sanjeev diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index caa1000cf..5c33e1a4f 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.9.4" +version = "0.10.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/external/CHANGELOG.md b/crates/external/CHANGELOG.md index 48fddf2eb..d1873c3e6 100644 --- a/crates/external/CHANGELOG.md +++ b/crates/external/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## external-v0.4.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + ## external-v0.3.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index 6a654050e..f3dc37bad 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "external" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index fa49e8f3c..8e960efe0 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.12.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + ## service-utils-v0.11.0 - 2024-02-20 #### Features - support for service prefix - (a2915b4) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index f1635d904..fefa91d02 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.11.0" +version = "0.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 39bd09040c24480525cd060560de4b4b8aa2288c Mon Sep 17 00:00:00 2001 From: Natarajan Kannan <natarajan@juspay.in> Date: Thu, 8 Feb 2024 19:25:04 +0530 Subject: [PATCH 302/352] docs: add intro doc and features feat: added HS cac client fix: a time bug with last_modified feat: added experimentation client feat: a working nix build feat: migrate from naersk to crane feat: flake parts added feat: nixified hs-cac-client feat: completed nixification --- .gitignore | 4 +- Cargo.lock | 88 ++++++- clients/hs-cac-client/CHANGELOG.md | 11 + clients/hs-cac-client/LICENSE | 30 +++ clients/hs-cac-client/README.md | 1 + clients/hs-cac-client/Setup.hs | 2 + clients/hs-cac-client/default.nix | 37 +++ clients/hs-cac-client/hs-cac-client.cabal | 118 +++++++++ clients/hs-cac-client/src/Client.hs | 121 +++++++++ clients/hs-cac-client/src/Main.hs | 26 ++ clients/hs-exp-client/CHANGELOG.md | 11 + clients/hs-exp-client/LICENSE | 30 +++ clients/hs-exp-client/README.md | 1 + clients/hs-exp-client/Setup.hs | 2 + clients/hs-exp-client/default.nix | 37 +++ clients/hs-exp-client/hs-exp-client.cabal | 119 +++++++++ clients/hs-exp-client/src/Client.hs | 128 ++++++++++ clients/hs-exp-client/src/Main.hs | 34 +++ {libs => clients}/js/index.js | 0 {libs => clients}/js/package-lock.json | 0 {libs => clients}/js/package.json | 0 {libs => clients}/js/src/index.ts | 0 {libs => clients}/js/src/types.ts | 0 {libs => clients}/js/src/utils/deepMerge.ts | 0 {libs => clients}/js/src/utils/operations.ts | 0 {libs => clients}/js/tsconfig.json | 0 {libs => clients}/js/webpack.config.js | 0 crates/cac_client/Cargo.toml | 9 + crates/cac_client/build.rs | 11 + crates/cac_client/default.nix | 63 +++++ crates/cac_client/rust-toolchain.toml | 2 + crates/cac_client/src/interface.rs | 219 ++++++++++++++++ crates/cac_client/src/lib.rs | 15 ++ .../src/api/config/handlers.rs | 9 +- .../experimentation-platform/src/db/schema.rs | 2 +- crates/superposition_client/Cargo.toml | 7 + crates/superposition_client/build.rs | 10 + crates/superposition_client/default.nix | 60 +++++ .../superposition_client/rust-toolchain.toml | 2 + crates/superposition_client/src/interface.rs | 240 ++++++++++++++++++ crates/superposition_client/src/lib.rs | 1 + crates/superposition_client/src/types.rs | 2 + flake.lock | 111 ++++---- flake.nix | 120 ++++++--- headers/libcac_client.h | 29 +++ headers/libsuperposition_client.h | 26 ++ libs | 1 + 47 files changed, 1642 insertions(+), 97 deletions(-) create mode 100644 clients/hs-cac-client/CHANGELOG.md create mode 100644 clients/hs-cac-client/LICENSE create mode 100644 clients/hs-cac-client/README.md create mode 100644 clients/hs-cac-client/Setup.hs create mode 100644 clients/hs-cac-client/default.nix create mode 100644 clients/hs-cac-client/hs-cac-client.cabal create mode 100644 clients/hs-cac-client/src/Client.hs create mode 100644 clients/hs-cac-client/src/Main.hs create mode 100644 clients/hs-exp-client/CHANGELOG.md create mode 100644 clients/hs-exp-client/LICENSE create mode 100644 clients/hs-exp-client/README.md create mode 100644 clients/hs-exp-client/Setup.hs create mode 100644 clients/hs-exp-client/default.nix create mode 100644 clients/hs-exp-client/hs-exp-client.cabal create mode 100644 clients/hs-exp-client/src/Client.hs create mode 100644 clients/hs-exp-client/src/Main.hs rename {libs => clients}/js/index.js (100%) rename {libs => clients}/js/package-lock.json (100%) rename {libs => clients}/js/package.json (100%) rename {libs => clients}/js/src/index.ts (100%) rename {libs => clients}/js/src/types.ts (100%) rename {libs => clients}/js/src/utils/deepMerge.ts (100%) rename {libs => clients}/js/src/utils/operations.ts (100%) rename {libs => clients}/js/tsconfig.json (100%) rename {libs => clients}/js/webpack.config.js (100%) create mode 100644 crates/cac_client/build.rs create mode 100644 crates/cac_client/default.nix create mode 100644 crates/cac_client/rust-toolchain.toml create mode 100644 crates/cac_client/src/interface.rs create mode 100644 crates/superposition_client/build.rs create mode 100644 crates/superposition_client/default.nix create mode 100644 crates/superposition_client/rust-toolchain.toml create mode 100644 crates/superposition_client/src/interface.rs create mode 100644 headers/libcac_client.h create mode 100644 headers/libsuperposition_client.h create mode 120000 libs diff --git a/.gitignore b/.gitignore index aa9ad0258..d975e813d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ backend/logfile backend/*PGSQL* backend/.env .direnv +/result +/result-* # dev bacon.toml @@ -27,4 +29,4 @@ docker-compose/localstack/export_cyphers.sh test_logs .keep .vscode -*.session.sql \ No newline at end of file +*.session.sql diff --git a/Cargo.lock b/Cargo.lock index 9b4eeb40d..ab475ae79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,6 +613,7 @@ name = "cac_client" version = "0.6.0" dependencies = [ "actix-web", + "cbindgen", "chrono", "derive_more", "jsonlogic", @@ -623,6 +624,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "tokio", ] [[package]] @@ -682,6 +684,25 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +[[package]] +name = "cbindgen" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" +dependencies = [ + "clap 3.2.25", + "heck", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.0.79" @@ -739,6 +760,21 @@ dependencies = [ "half", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.3.4" @@ -759,7 +795,7 @@ dependencies = [ "anstream", "anstyle", "bitflags 1.3.2", - "clap_lex", + "clap_lex 0.5.0", "strsim", ] @@ -775,6 +811,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.5.0" @@ -1651,7 +1696,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.0.2", "slab", "tokio", "tokio-util", @@ -1664,6 +1709,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.13.2" @@ -1872,6 +1923,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.0.2" @@ -2018,7 +2079,7 @@ dependencies = [ "anyhow", "base64 0.21.2", "bytecount", - "clap", + "clap 4.3.4", "fancy-regex", "fraction", "getrandom", @@ -2115,7 +2176,7 @@ dependencies = [ "futures", "getrandom", "html-escape", - "indexmap", + "indexmap 2.0.2", "itertools 0.12.1", "js-sys", "leptos_reactive", @@ -2141,7 +2202,7 @@ checksum = "3f62dcab17728877f2d2f16d2c8a6701c4c5fbdfb4964792924acb0b50529659" dependencies = [ "anyhow", "camino", - "indexmap", + "indexmap 2.0.2", "parking_lot", "proc-macro2", "quote", @@ -2195,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc25c0f7f14ed5daf42b8d0d273ed790b0449e8ba8cff1c2fa800dc90a75acb" dependencies = [ "cfg-if", - "indexmap", + "indexmap 2.0.2", "leptos", "tracing", "wasm-bindgen", @@ -2211,7 +2272,7 @@ dependencies = [ "base64 0.21.2", "cfg-if", "futures", - "indexmap", + "indexmap 2.0.2", "js-sys", "paste", "pin-project", @@ -2665,6 +2726,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "overload" version = "0.1.1" @@ -3631,6 +3698,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" name = "superposition_client" version = "0.4.0" dependencies = [ + "cbindgen", "chrono", "derive_more", "dotenv", @@ -3768,6 +3836,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.40" diff --git a/clients/hs-cac-client/CHANGELOG.md b/clients/hs-cac-client/CHANGELOG.md new file mode 100644 index 000000000..9d0666ba0 --- /dev/null +++ b/clients/hs-cac-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-cac-client/LICENSE b/clients/hs-cac-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/hs-cac-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-cac-client/README.md b/clients/hs-cac-client/README.md new file mode 100644 index 000000000..1c2d58957 --- /dev/null +++ b/clients/hs-cac-client/README.md @@ -0,0 +1 @@ +# hs-cac-client diff --git a/clients/hs-cac-client/Setup.hs b/clients/hs-cac-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/hs-cac-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/hs-cac-client/default.nix b/clients/hs-cac-client/default.nix new file mode 100644 index 000000000..b67cf868f --- /dev/null +++ b/clients/hs-cac-client/default.nix @@ -0,0 +1,37 @@ +{ + perSystem = { config, pkgs, lib, self', ... }: { + haskellProjects.cac = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + cac_client.custom = _: self'.packages.clients; + }; + }; + packages.hs-cac-client = self'.packages.cac-hs-cac-client.overrideAttrs (oa: { + cac_client = self'.packages.clients; + nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ + pkgs.makeWrapper + ]; + # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 + postFixup = (oa.postFixup or "") + '' + wrapProgram $out/bin/* \ + --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $cac_client/lib + ''; + }); + apps.cac.program = lib.getExe self'.packages.cac; + + devShells.cac-client = pkgs.mkShell { + name = "hs-cac-client"; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + config.flake-root.devShell + ]; + # TODO: set once, based on platform + # TODO(refactor): SRID: can we do this in one place? + # LD_LIBRARY_PATH = "${self'.packages.cac_client}/lib"; + shellHook = '' + export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" + ''; + }; + }; +} diff --git a/clients/hs-cac-client/hs-cac-client.cabal b/clients/hs-cac-client/hs-cac-client.cabal new file mode 100644 index 000000000..3d70ba9a0 --- /dev/null +++ b/clients/hs-cac-client/hs-cac-client.cabal @@ -0,0 +1,118 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-cac-client +version: 0.1.0.0 +description: Please see the README on GitHub at <https://github.com/githubuser/hs#readme> +homepage: https://github.com/githubuser/hs#readme +bug-reports: https://github.com/githubuser/hs/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs + +library + exposed-modules: + Client + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exe + main-is: Main.hs + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + cac_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-cac-client + default-language: Haskell2010 diff --git a/clients/hs-cac-client/src/Client.hs b/clients/hs-cac-client/src/Client.hs new file mode 100644 index 000000000..eb0aaefaa --- /dev/null +++ b/clients/hs-cac-client/src/Client.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( createCacClient +, getCacClient +, getCacConfig +, getCacLastModified +, cacEval +, cacStartPolling +) where + +import Data.Aeson +import Data.Functor (($>)) +import Foreign.C.String (CString, newCAString, peekCAString) +import Foreign.C.Types (CInt (CInt), CULong (..)) +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Ptr +import Prelude + +data Arc_Client + +type CacClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_cac_client :: CTenant -> Bool -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_cac_client :: FunPtr (Ptr CacClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_cac_client :: CTenant -> IO (Ptr CacClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "get_last_modified" + c_get_last_modified_time :: Ptr CacClient -> IO CString + +foreign import ccall unsafe "get_config" + c_get_config :: Ptr CacClient -> IO CString + +foreign import ccall unsafe "cac_eval" + c_cac_eval :: Ptr CacClient -> CString -> IO CString + +foreign import ccall safe "start_polling_update" + c_cac_poll :: CTenant -> IO () + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +cacStartPolling :: Tenant -> IO () +cacStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_cac_poll + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createCacClient:: Tenant -> Bool -> Integer -> String -> IO (Either Error ()) +createCacClient tenant shouldUpdate frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_cac_client cTenant shouldUpdate duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) +getCacClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_cac_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_cac_client cacClient + +getCacConfig :: ForeignPtr CacClient -> IO (Either Error Value) +getCacConfig client = do + config <- withForeignPtr client c_get_config + if config == nullPtr + then Left <$> getError + else do + fptrConfig <- newForeignPtr c_free_string config + Right . toJSON <$> withForeignPtr fptrConfig peekCAString + +getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) +getCacLastModified client = do + lastModified <- withForeignPtr client c_get_last_modified_time + if lastModified == nullPtr + then Left <$> getError + else do + fptrLastModified <- newForeignPtr c_free_string lastModified + Right <$> withForeignPtr fptrLastModified peekCAString + +cacEval :: ForeignPtr CacClient -> String -> IO (Either Error Value) +cacEval client context = do + cContext <- newCAString context + overrides <- withForeignPtr client (`c_cac_eval` cContext) + _ <- cleanup [cContext] + if overrides == nullPtr + then Left <$> getError + else do + fptrOverrides <- newForeignPtr c_free_string overrides + Right . toJSON <$> withForeignPtr fptrOverrides peekCAString diff --git a/clients/hs-cac-client/src/Main.hs b/clients/hs-cac-client/src/Main.hs new file mode 100644 index 000000000..d3530ab1d --- /dev/null +++ b/clients/hs-cac-client/src/Main.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (cacEval, createCacClient, getCacClient, + getCacConfig, getCacLastModified, cacStartPolling) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createCacClient "dev" True 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkOS (cacStartPolling "dev") + print threadId + getCacClient "dev" >>= \case + Left err -> putStrLn err + Right client -> do + config <- getCacConfig client + lastModified <- getCacLastModified client + overrides <- cacEval client "{\"os\": \"android\", \"client\": \"2mg\"}" + print config + print lastModified + print overrides + threadDelay 1000000000 + pure () diff --git a/clients/hs-exp-client/CHANGELOG.md b/clients/hs-exp-client/CHANGELOG.md new file mode 100644 index 000000000..e43174087 --- /dev/null +++ b/clients/hs-exp-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs-exp-client` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-exp-client/LICENSE b/clients/hs-exp-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/hs-exp-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-exp-client/README.md b/clients/hs-exp-client/README.md new file mode 100644 index 000000000..8401db649 --- /dev/null +++ b/clients/hs-exp-client/README.md @@ -0,0 +1 @@ +# hs-exp-client diff --git a/clients/hs-exp-client/Setup.hs b/clients/hs-exp-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/hs-exp-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/hs-exp-client/default.nix b/clients/hs-exp-client/default.nix new file mode 100644 index 000000000..1c6136789 --- /dev/null +++ b/clients/hs-exp-client/default.nix @@ -0,0 +1,37 @@ +{ + perSystem = { config, pkgs, lib, self', ... }: { + haskellProjects.exp = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + superposition_client.custom = _: self'.packages.clients; + }; + }; + packages.hs-exp-client = self'.packages.exp-hs-exp-client.overrideAttrs (oa: { + superposition_client = self'.packages.clients; + nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ + pkgs.makeWrapper + ]; + # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 + postFixup = (oa.postFixup or "") + '' + wrapProgram $out/bin/* \ + --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $superposition_client/lib + ''; + }); + apps.cac.program = lib.getExe self'.packages.cac; + + devShells.exp-client = pkgs.mkShell { + name = "hs-exp-client"; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + config.flake-root.devShell + ]; + # TODO: set once, based on platform + # TODO(refactor): SRID: can we do this in one place? + # LD_LIBRARY_PATH = "${self'.packages.superposition_client}/lib"; + shellHook = '' + export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" + ''; + }; + }; +} diff --git a/clients/hs-exp-client/hs-exp-client.cabal b/clients/hs-exp-client/hs-exp-client.cabal new file mode 100644 index 000000000..cfa266e54 --- /dev/null +++ b/clients/hs-exp-client/hs-exp-client.cabal @@ -0,0 +1,119 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-exp-client +version: 0.1.0.0 +description: Please see the README on GitHub at <https://github.com/githubuser/hs-exp-client#readme> +homepage: https://github.com/githubuser/hs-exp-client#readme +bug-reports: https://github.com/githubuser/hs-exp-client/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs-exp-client + +library + exposed-modules: + + Client + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exp-client-exe + main-is: Main.hs + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + superposition_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-exp-client + default-language: Haskell2010 diff --git a/clients/hs-exp-client/src/Client.hs b/clients/hs-exp-client/src/Client.hs new file mode 100644 index 000000000..7e28353e9 --- /dev/null +++ b/clients/hs-exp-client/src/Client.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( expStartPolling +, getExpClient +, createExpClient +, getApplicableVariants +, getSatisfiedExperiments +, getRunningExperiments +) where + +import Data.Aeson.Types +import Data.Functor (($>)) +import Foreign (FunPtr, Ptr) +import Foreign.C (CInt (..), CShort (..), CULong (..)) +import Foreign.C.String +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Ptr (nullPtr) +import Prelude + +data Arc_Client + +type ExpClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_exp_client :: CTenant -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_exp_client :: FunPtr (Ptr ExpClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_exp_client :: CTenant -> IO (Ptr ExpClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +foreign import ccall unsafe "start_polling_update" + c_start_polling_update :: CTenant -> IO () + +foreign import ccall unsafe "get_applicable_variant" + c_get_applicable_variants :: Ptr ExpClient -> CString -> CShort -> IO CString + +foreign import ccall unsafe "get_satisfied_experiments" + c_get_satisfied_experiments :: Ptr ExpClient -> CString -> IO CString + +foreign import ccall unsafe "get_running_experiments" + c_get_running_experiments :: Ptr ExpClient -> IO CString + +expStartPolling :: Tenant -> IO () +expStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_start_polling_update + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) +createExpClient tenant frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_exp_client cTenant duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) +getExpClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_exp_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_exp_client cacClient + +getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +getApplicableVariants client query toss = do + context <- newCAString query + variants <- withForeignPtr client (\c -> c_get_applicable_variants c context (fromInteger toss)) + _ <- cleanup [context] + if variants == nullPtr + then Left <$> getError + else do + fptrVariants <- newForeignPtr c_free_string variants + Right <$> withForeignPtr fptrVariants peekCAString + -- pure $ + -- case fromJSON variantVector of + -- Error s -> Left s + -- Success vec -> Right vec + +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +getSatisfiedExperiments client query = do + context <- newCAString query + experiments <- withForeignPtr client (`c_get_satisfied_experiments` context) + _ <- cleanup [context] + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString + +getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) +getRunningExperiments client = do + experiments <- withForeignPtr client c_get_running_experiments + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString diff --git a/clients/hs-exp-client/src/Main.hs b/clients/hs-exp-client/src/Main.hs new file mode 100644 index 000000000..bb7621266 --- /dev/null +++ b/clients/hs-exp-client/src/Main.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (createExpClient, expStartPolling, + getApplicableVariants, getExpClient, + getRunningExperiments, + getSatisfiedExperiments) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createExpClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkIO (expStartPolling "dev") + print threadId + getExpClient "dev" >>= \case + Left err -> putStrLn err + Right client -> loop client + pure () + where + loop client = do + runningExperiments <- getRunningExperiments client + satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" + variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + print "Running experiments" + print runningExperiments + print "experiments that satisfy context" + print satisfiedExperiments + print "variant ID applied" + print variants + -- threadDelay 10000000 + loop client diff --git a/libs/js/index.js b/clients/js/index.js similarity index 100% rename from libs/js/index.js rename to clients/js/index.js diff --git a/libs/js/package-lock.json b/clients/js/package-lock.json similarity index 100% rename from libs/js/package-lock.json rename to clients/js/package-lock.json diff --git a/libs/js/package.json b/clients/js/package.json similarity index 100% rename from libs/js/package.json rename to clients/js/package.json diff --git a/libs/js/src/index.ts b/clients/js/src/index.ts similarity index 100% rename from libs/js/src/index.ts rename to clients/js/src/index.ts diff --git a/libs/js/src/types.ts b/clients/js/src/types.ts similarity index 100% rename from libs/js/src/types.ts rename to clients/js/src/types.ts diff --git a/libs/js/src/utils/deepMerge.ts b/clients/js/src/utils/deepMerge.ts similarity index 100% rename from libs/js/src/utils/deepMerge.ts rename to clients/js/src/utils/deepMerge.ts diff --git a/libs/js/src/utils/operations.ts b/clients/js/src/utils/operations.ts similarity index 100% rename from libs/js/src/utils/operations.ts rename to clients/js/src/utils/operations.ts diff --git a/libs/js/tsconfig.json b/clients/js/tsconfig.json similarity index 100% rename from libs/js/tsconfig.json rename to clients/js/tsconfig.json diff --git a/libs/js/webpack.config.js b/clients/js/webpack.config.js similarity index 100% rename from libs/js/webpack.config.js rename to clients/js/webpack.config.js diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index fbd224d34..ae6a8666a 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -2,6 +2,7 @@ name = "cac_client" version = "0.6.0" edition = "2021" +build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -17,3 +18,11 @@ serde_json = { workspace = true } log = { workspace = true } strum_macros = { workspace = true } strum = { workspace = true } +tokio = {version = "1.29.1", features = ["full"]} + +[lib] +name = "cac_client" +crate-type = ["cdylib", "lib"] + +[build-dependencies] +cbindgen = "0.26.0" diff --git a/crates/cac_client/build.rs b/crates/cac_client/build.rs new file mode 100644 index 000000000..b1eb06cc8 --- /dev/null +++ b/crates/cac_client/build.rs @@ -0,0 +1,11 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + println!("Calling build.rs in cac_client"); + cbindgen::generate_with_config(&crate_dir, config) + .unwrap() + .write_to_file("../../headers/libcac_client.h"); +} \ No newline at end of file diff --git a/crates/cac_client/default.nix b/crates/cac_client/default.nix new file mode 100644 index 000000000..b0c2af779 --- /dev/null +++ b/crates/cac_client/default.nix @@ -0,0 +1,63 @@ +# Nix for Rust project management +{ inputs, ... }: { + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + args = { + pname = "cac_client"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cargoVendorDir = craneLib.vendorCargoDeps { + src = ./../../.; + }; + cargoArtifacts = craneLib.buildDepsOnly args; + package = craneLib.buildPackage (args // { + inherit cargoArtifacts; + doCheck = false; # FIXME: tests require services to be running + }); + + check = craneLib.cargoClippy (args // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + packages.cac_client = package; + + checks.clippy = check; + + # # Flake outputs + # devShells.rust-exp = pkgs.mkShell { + # inputsFrom = [ + # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + # ]; + # shellHook = '' + # # For rust-analyzer 'hover' tooltips to work. + # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + # ''; + # nativeBuildInputs = with pkgs; [ + # # Add your dev tools here. + # rustToolchain + # cargo-watch + # ]; + # }; + }; +} diff --git a/crates/cac_client/rust-toolchain.toml b/crates/cac_client/rust-toolchain.toml new file mode 100644 index 000000000..31578d3bf --- /dev/null +++ b/crates/cac_client/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs new file mode 100644 index 000000000..f79a1cffc --- /dev/null +++ b/crates/cac_client/src/interface.rs @@ -0,0 +1,219 @@ +// Primary interface so CAC client can work with other languages like haskell +#[warn(unused_assignments)] +use std::{ + ffi::{c_char, c_ulong, CStr}, + sync::Arc, +}; + +use crate::{utils::core::MapError, Client, CLIENT_FACTORY}; +use serde_json::{Map, Value}; +use std::{ + cell::RefCell, + ffi::{c_int, CString}, + time::Duration, +}; +use tokio::{runtime::Runtime, task}; + +thread_local! { + static LAST_ERROR: RefCell<Option<Box<String>>> = RefCell::new(None); +} + +fn cstring_to_rstring(s: *const c_char) -> Result<String, String> { + let s = unsafe { CStr::from_ptr(s) }; + s.to_str().map(str::to_string).map_err_to_string() +} + +fn rstring_to_cstring(s: String) -> CString { + CString::new(s.as_str()).unwrap_or_default() +} + +pub fn update_last_error(err: String) { + println!("Setting LAST_ERROR: {}", err); + + LAST_ERROR.with(|prev| { + *prev.borrow_mut() = Some(Box::new(err)); + }); +} + +pub fn take_last_error() -> Option<Box<String>> { + LAST_ERROR.with(|prev| prev.borrow_mut().take()) +} + +#[no_mangle] +pub extern "C" fn last_error_length() -> c_int { + LAST_ERROR.with(|prev| match *prev.borrow() { + Some(ref err) => err.to_string().len() as c_int + 1, + None => 0, + }) +} + +#[no_mangle] +pub unsafe extern "C" fn last_error_message() -> *const c_char { + let last_error = match take_last_error() { + Some(err) => err, + None => return std::ptr::null_mut(), + }; + let error_message = last_error.to_string(); + // println!("Error in last_error_message {error_message}"); + let err = rstring_to_cstring(error_message); + err.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn free_string(s: *mut c_char) { + if s.is_null() { + return; + } + unsafe { + let _ = CString::from_raw(s); + } +} + +#[no_mangle] +pub extern "C" fn new_client( + tenant: *const c_char, + update_periodically: bool, + update_frequency: c_ulong, + hostname: *const c_char, +) -> c_int { + let duration = Duration::new(update_frequency, 0); + let tenant = match cstring_to_rstring(tenant) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + let hostname = match cstring_to_rstring(hostname) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + + // println!("Creating cac client thread for tenant {tenant}"); + let local = task::LocalSet::new(); + local.block_on(&Runtime::new().unwrap(), async move { + match CLIENT_FACTORY + .create_client(tenant.clone(), update_periodically, duration, hostname) + .await + { + Ok(_) => return 0, + Err(err) => { + update_last_error(err); + return 1; + } + } + }); + return 0; +} + +#[no_mangle] +pub extern "C" fn start_polling_update(tenant: *const c_char) { + if tenant.is_null() { + return (); + } + unsafe { + let client = get_client(tenant); + let local = task::LocalSet::new(); + // println!("in FFI polling"); + local.block_on( + &Runtime::new().unwrap(), + (**client).clone().ffi_polling_update(), + ); + } +} + +#[no_mangle] +pub extern "C" fn free_client(ptr: *mut Arc<Client>) { + if ptr.is_null() { + return; + } + unsafe { + let _ = Box::from_raw(ptr); + } +} + +#[no_mangle] +pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc<Client> { + let ten = match cstring_to_rstring(tenant) { + Ok(t) => t, + Err(err) => { + update_last_error(err); + return std::ptr::null_mut(); + } + }; + // println!("fetching cac client thread for tenant {ten}"); + match CLIENT_FACTORY.get_client(ten).map_err(|e| format!("{}", e)) { + Ok(client) => Box::into_raw(Box::new(client)), + Err(err) => { + // println!("error occurred {err}"); + update_last_error(err); + // println!("error set"); + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub extern "C" fn cac_eval( + client: *mut Arc<Client>, + query: *const c_char, +) -> *const c_char { + let context_string = match cstring_to_rstring(query) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + let context: Map<String, Value> = + match serde_json::from_str::<Map<String, Value>>(context_string.as_str()) { + Ok(json) => json, + Err(err) => { + update_last_error(err.to_string()); + return std::ptr::null(); + } + }; + let overrides = match unsafe { (*client).eval(context) } { + Ok(ov) => match serde_json::to_string::<Map<String, Value>>(&ov) { + Ok(ove) => ove, + Err(err) => { + update_last_error(err.to_string()); + return std::ptr::null(); + } + }, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + rstring_to_cstring(overrides).into_raw() +} + +#[no_mangle] +pub extern "C" fn get_last_modified(client: *mut Arc<Client>) -> *const c_char { + let last_modified = unsafe { (*client).get_last_modified() }; + match last_modified { + Ok(date) => rstring_to_cstring(date.to_string()).into_raw(), + Err(err) => { + update_last_error(err); + std::ptr::null() + } + } +} + +#[no_mangle] +pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { + let config = unsafe { (*client).get_config() }; + match config { + Ok(c) => { + rstring_to_cstring(serde_json::to_value(c).unwrap().to_string()).into_raw() + } + Err(err) => { + update_last_error(err); + std::ptr::null_mut() + } + } +} diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 8804984ef..75b560d59 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -1,4 +1,5 @@ mod eval; +mod interface; mod utils; use actix_web::{ @@ -25,6 +26,7 @@ pub struct Context { pub override_with_keys: [String; 1], } +#[repr(C)] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { contexts: Vec<Context>, @@ -45,6 +47,7 @@ impl Default for MergeStrategy { } } +#[repr(C)] #[derive(Clone)] pub struct Client { tenant: String, @@ -147,6 +150,18 @@ impl Client { }); } + pub async fn ffi_polling_update(self) { + println!("into polling updates"); + let mut interval = interval(self.polling_interval); + loop { + println!("started polling updates"); + interval.tick().await; + let result = self.update_cac().await.unwrap_or_else(identity); + println!("{result}"); + log::info!("{result}",); + } + } + pub fn get_config(&self) -> Result<Config, String> { self.config.read().map(|c| c.clone()).map_err_to_string() } diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 3bd16e04a..c574245cc 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -60,6 +60,7 @@ fn is_not_modified( max_created_at: Option<NaiveDateTime>, req: &HttpRequest, ) -> anyhow::Result<bool> { + let nanosecond_erasure = |t: NaiveDateTime| t.with_nanosecond(0); let last_modified = req .headers() .get("If-Modified-Since") @@ -69,8 +70,10 @@ fn is_not_modified( .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) .ok() }) - .and_then(|t| t.with_nanosecond(0)); - Ok(max_created_at.is_some() && max_created_at <= last_modified) + .and_then(nanosecond_erasure); + log::info!("last modified {last_modified:?}"); + let parsed_max: Option<NaiveDateTime> = max_created_at.and_then(nanosecond_erasure); + Ok(max_created_at.is_some() && parsed_max <= last_modified) } async fn generate_cac( @@ -124,6 +127,8 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR .map_err(|e| log::error!("failed to fetch max timestamp from event_log: {e}")) .ok(); + log::info!("Max created at: {max_created_at:?}"); + let is_not_modified = is_not_modified(max_created_at, &req) .map_err(|e| log::error!("config not modified: {e}")); diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index a8676f480..6cd391ae0 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -616,4 +616,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m11, event_log_y2026m12, experiments, -); +); \ No newline at end of file diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index 443da4114..bec562de6 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -14,3 +14,10 @@ tokio = {version = "1.29.1", features = ["full"]} dotenv = { workspace = true } derive_more = { workspace = true } log = { workspace = true } + +[lib] +name = "superposition_client" +crate-type = ["cdylib", "lib"] + +[build-dependencies] +cbindgen = "0.26.0" \ No newline at end of file diff --git a/crates/superposition_client/build.rs b/crates/superposition_client/build.rs new file mode 100644 index 000000000..e72f5872b --- /dev/null +++ b/crates/superposition_client/build.rs @@ -0,0 +1,10 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + cbindgen::generate_with_config(&crate_dir, config) + .unwrap() + .write_to_file("../../headers/libsuperposition_client.h"); +} \ No newline at end of file diff --git a/crates/superposition_client/default.nix b/crates/superposition_client/default.nix new file mode 100644 index 000000000..a78ee5e5d --- /dev/null +++ b/crates/superposition_client/default.nix @@ -0,0 +1,60 @@ +# Nix for Rust project management +{ inputs, ... }: { + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + args = { + pname = "superposition_client"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cargoArtifacts = craneLib.buildDepsOnly args; + package = craneLib.buildPackage (args // { + inherit cargoArtifacts; + doCheck = false; # FIXME: tests require services to be running + }); + + check = craneLib.cargoClippy (args // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + packages.superposition_client = package; + + checks.clippy = check; + + # # Flake outputs + # devShells.rust-exp = pkgs.mkShell { + # inputsFrom = [ + # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + # ]; + # shellHook = '' + # # For rust-analyzer 'hover' tooltips to work. + # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + # ''; + # nativeBuildInputs = with pkgs; [ + # # Add your dev tools here. + # rustToolchain + # cargo-watch + # ]; + # }; + }; +} diff --git a/crates/superposition_client/rust-toolchain.toml b/crates/superposition_client/rust-toolchain.toml new file mode 100644 index 000000000..31578d3bf --- /dev/null +++ b/crates/superposition_client/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file diff --git a/crates/superposition_client/src/interface.rs b/crates/superposition_client/src/interface.rs new file mode 100644 index 000000000..7ace9d349 --- /dev/null +++ b/crates/superposition_client/src/interface.rs @@ -0,0 +1,240 @@ +#[warn(unused_assignments)] +use std::{ + ffi::{c_char, c_ulong, CStr}, + sync::Arc, +}; + +use crate::{Client, CLIENT_FACTORY}; +use serde_json::Value; +use std::{ + cell::RefCell, + ffi::{c_int, c_short, CString}, +}; +use tokio::{runtime::Runtime, task}; + +thread_local! { + static LAST_ERROR: RefCell<Option<Box<String>>> = RefCell::new(None); +} + +fn to_string<E>(e: E) -> String +where + E: ToString, +{ + e.to_string() +} + +fn error_block<E>(err: String) -> *mut E { + update_last_error(err); + return std::ptr::null_mut(); +} + +fn cstring_to_rstring(s: *const c_char) -> Result<String, String> { + let s = unsafe { CStr::from_ptr(s) }; + s.to_str().map(str::to_string).map_err(to_string) +} + +fn rstring_to_cstring(s: String) -> CString { + CString::new(s.as_str()).unwrap_or_default() +} + +pub fn update_last_error(err: String) { + println!("Setting LAST_ERROR: {}", err); + + LAST_ERROR.with(|prev| { + *prev.borrow_mut() = Some(Box::new(err)); + }); +} + +pub fn take_last_error() -> Option<Box<String>> { + LAST_ERROR.with(|prev| prev.borrow_mut().take()) +} + +#[no_mangle] +pub extern "C" fn last_error_length() -> c_int { + LAST_ERROR.with(|prev| match *prev.borrow() { + Some(ref err) => err.to_string().len() as c_int + 1, + None => 0, + }) +} + +#[no_mangle] +pub unsafe extern "C" fn last_error_message() -> *const c_char { + let last_error = match take_last_error() { + Some(err) => err, + None => return std::ptr::null_mut(), + }; + let error_message = last_error.to_string(); + // println!("Error in last_error_message {error_message}"); + let err = rstring_to_cstring(error_message); + err.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn free_string(s: *mut c_char) { + if s.is_null() { + return; + } + unsafe { + let _ = CString::from_raw(s); + } +} + +#[no_mangle] +pub extern "C" fn new_client( + tenant: *const c_char, + update_frequency: c_ulong, + hostname: *const c_char, +) -> c_int { + let duration = update_frequency as u64; + let tenant = match cstring_to_rstring(tenant) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + let hostname = match cstring_to_rstring(hostname) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + + // println!("Creating cac client thread for tenant {tenant}"); + let local = task::LocalSet::new(); + local.block_on(&Runtime::new().unwrap(), async move { + match CLIENT_FACTORY + .create_client(tenant.clone(), duration, hostname) + .await + { + Ok(_) => return 0, + Err(err) => { + update_last_error(err); + return 1; + } + } + }); + return 0; +} + +#[no_mangle] +pub extern "C" fn start_polling_update(tenant: *const c_char) { + if tenant.is_null() { + return (); + } + unsafe { + let client = get_client(tenant); + let local = task::LocalSet::new(); + // println!("in FFI polling"); + local.block_on( + &Runtime::new().unwrap(), + (*client).clone().run_polling_updates(), + ); + } +} + +#[no_mangle] +pub extern "C" fn free_client(ptr: *mut Arc<Client>) { + if ptr.is_null() { + return; + } + unsafe { + let _ = Box::from_raw(ptr); + } +} + +#[no_mangle] +pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc<Client> { + let ten = match cstring_to_rstring(tenant) { + Ok(t) => t, + Err(err) => { + update_last_error(err); + return std::ptr::null_mut(); + } + }; + let local = task::LocalSet::new(); + local.block_on( + &Runtime::new().unwrap(), + // println!("fetching exp client thread for tenant {ten}"); + async move { + match CLIENT_FACTORY.get_client(ten).await { + Ok(client) => Box::into_raw(Box::new(client)), + Err(err) => { + // println!("error occurred {err}"); + update_last_error(err); + // println!("error set"); + std::ptr::null_mut() + } + } + }, + ) +} + +#[no_mangle] +pub extern "C" fn get_applicable_variant( + client: *mut Arc<Client>, + c_context: *const c_char, + toss: c_short, +) -> *mut c_char { + let context = match cstring_to_rstring(c_context) { + Ok(c) => match serde_json::from_str::<Value>(c.as_str()) { + Ok(con) => con, + Err(err) => return error_block(err.to_string()), + }, + Err(err) => return error_block(err), + }; + // println!("Fetching variantIds"); + let local = task::LocalSet::new(); + let variants = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_applicable_variant(&context, toss as i8) + }); + // println!("variantIds: {:?}", variants); + match serde_json::to_string::<Vec<String>>(&variants) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => return error_block(err.to_string()), + } +} + +#[no_mangle] +pub extern "C" fn get_satisfied_experiments( + client: *mut Arc<Client>, + c_context: *const c_char, +) -> *mut c_char { + let context = match cstring_to_rstring(c_context) { + Ok(c) => match serde_json::from_str::<Value>(c.as_str()) { + Ok(con) => con, + Err(err) => return error_block(err.to_string()), + }, + Err(err) => return error_block(err), + }; + + let local = task::LocalSet::new(); + let experiments = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_satisfied_experiments(&context) + }); + let experiments = match serde_json::to_value(experiments) { + Ok(value) => value, + Err(err) => return error_block(err.to_string()), + }; + match serde_json::to_string(&experiments) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => return error_block(err.to_string()), + } +} + +#[no_mangle] +pub extern "C" fn get_running_experiments(client: *mut Arc<Client>) -> *mut c_char { + let local = task::LocalSet::new(); + let experiments = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_running_experiments() + }); + let experiments = match serde_json::to_value(experiments) { + Ok(value) => value, + Err(err) => return error_block(err.to_string()), + }; + match serde_json::to_string(&experiments) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => return error_block(err.to_string()), + } +} diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 8eb569178..9c58a59bf 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -1,4 +1,5 @@ mod types; +mod interface; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; diff --git a/crates/superposition_client/src/types.rs b/crates/superposition_client/src/types.rs index a53c668a0..b12b9cef3 100644 --- a/crates/superposition_client/src/types.rs +++ b/crates/superposition_client/src/types.rs @@ -23,6 +23,7 @@ pub(crate) enum VariantType { EXPERIMENTAL, } +#[repr(C)] #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Variant { pub id: String, @@ -32,6 +33,7 @@ pub struct Variant { pub type Variants = Vec<Variant>; +#[repr(C)] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Experiment { pub variants: Variants, diff --git a/flake.lock b/flake.lock index 68e6b01f4..2aea8b76d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,26 +1,46 @@ { "nodes": { - "flake-utils": { + "crane": { "inputs": { - "systems": "systems" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "lastModified": 1705974079, + "narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=", + "owner": "ipetkov", + "repo": "crane", + "rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1704982712, + "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "07f6395285469419cf9d078f59b5b49993198c00", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "flake-utils_2": { + "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1705309234, @@ -36,55 +56,56 @@ "type": "github" } }, - "naersk": { - "inputs": { - "nixpkgs": "nixpkgs" - }, + "haskell-flake": { "locked": { - "lastModified": 1698420672, - "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", - "owner": "nix-community", - "repo": "naersk", - "rev": "aeb58d5e8faead8980a807c840232697982d47b9", + "lastModified": 1705984248, + "narHash": "sha256-1zW2udPF4clKCuTgk0FHjGUB7x/Tfe8m9oUhe3hM/YQ=", + "owner": "srid", + "repo": "haskell-flake", + "rev": "cea3332b027264e914673df392870ebb1e92de44", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "naersk", + "owner": "srid", + "repo": "haskell-flake", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1707743206, - "narHash": "sha256-AehgH64b28yKobC/DAWYZWkJBxL/vP83vkY+ag2Hhy4=", + "lastModified": 1706550542, + "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2d627a2a704708673e56346fcb13d25344b8eaf3", + "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", "type": "github" }, "original": { - "id": "nixpkgs", - "type": "indirect" + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" } }, - "nixpkgs_2": { + "nixpkgs-lib": { "locked": { - "lastModified": 1707689078, - "narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=", + "dir": "lib", + "lastModified": 1703961334, + "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8", + "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", "type": "github" }, "original": { + "dir": "lib", "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_2": { "locked": { "lastModified": 1706487304, "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", @@ -102,16 +123,17 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", - "naersk": "naersk", - "nixpkgs": "nixpkgs_2", + "crane": "crane", + "flake-parts": "flake-parts", + "haskell-flake": "haskell-flake", + "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_3" + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1707790272, @@ -141,21 +163,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 8290821da..9ead5676c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,39 +1,91 @@ { inputs = { - flake-utils.url = "github:numtide/flake-utils"; - naersk.url = "github:nix-community/naersk"; + flake-parts.url = "github:hercules-ci/flake-parts"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + crane.url = "github:ipetkov/crane"; + crane.inputs.nixpkgs.follows = "nixpkgs"; + haskell-flake.url = "github:srid/haskell-flake"; rust-overlay = { url = "github:oxalica/rust-overlay"; }; }; - outputs = { self,rust-overlay, flake-utils, naersk, nixpkgs }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = (import nixpkgs) { - inherit system; - overlays = [ rust-overlay.overlay ]; - }; + outputs = inputs@{ self, rust-overlay, flake-parts, crane, nixpkgs, haskell-flake, ... }: - naersk' = pkgs.callPackage naersk {}; + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ]; - in rec { - # For `nix build` & `nix run`: - defaultPackage = naersk'.buildPackage { - nativeBuildInputs = with pkgs; [ postgresql_12 ]; - src = ./.; - }; + imports = [ + inputs.haskell-flake.flakeModule + ./clients/hs-cac-client + ./clients/hs-exp-client + ]; + perSystem = { config, self', inputs', system, pkgs, lib, haskell-flake, ... }: + let + craneLib = crane.lib.${system}; + cac_args = { + pname = "cac_client"; + src = ./crates/cac_client; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + exp_args = { + pname = "superposition_client"; + src = ./crates/superposition_client; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cacCargoArtifacts = craneLib.buildDepsOnly cac_args; + expCargoArtifacts = craneLib.buildDepsOnly exp_args; + cac_client = craneLib.buildPackage (cac_args // { + inherit cacCargoArtifacts; + }); + superposition_client = craneLib.buildPackage (exp_args // { + inherit expCargoArtifacts; + }); + in + rec { + _module.args.pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlay ]; + + }; + formatter = pkgs.nixpkgs-fmt; + # For `nix build` & `nix run`: + + packages.clients = craneLib.buildPackage { + nativeBuildInputs = with pkgs; [ postgresql_12 iconv darwin.apple_sdk.frameworks.Security ]; + src = craneLib.path ./.; # FIXME: move rust stuff to subdir + }; + + # For `nix develop`: + devShells.default = pkgs.mkShell { + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + # bring your local shell properties into nix env + # shellHook = '' + # echo "you are now in the nix shell" + # eval $($SHELL) + # ''; + nativeBuildInputs = + let - # For `nix develop`: - devShell = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - # bring your local shell properties into nix env - # shellHook = '' - # echo "you are now in the nix shell" - # eval $($SHELL) - # ''; - nativeBuildInputs = - let - univPkgs = with pkgs; [ + univPkgs = with pkgs; [ # Build requirements cargo libiconv @@ -51,7 +103,7 @@ pkg-config awscli jq - nodejs_18 + pkgs.nodejs_18 leptosfmt wasm-pack curl @@ -60,13 +112,13 @@ targets = [ "wasm32-unknown-unknown" ]; }) ]; - darwinPkgs = with pkgs; [ + darwinPkgs = with pkgs; [ darwin.apple_sdk.frameworks.Security darwin.apple_sdk.frameworks.SystemConfiguration ]; - in - univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else []); + in + univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else [ ]); + }; }; - } - ); -} \ No newline at end of file + }; +} diff --git a/headers/libcac_client.h b/headers/libcac_client.h new file mode 100644 index 000000000..cf6ef0606 --- /dev/null +++ b/headers/libcac_client.h @@ -0,0 +1,29 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +typedef struct Arc_Client Arc_Client; + +int last_error_length(void); + +const char *last_error_message(void); + +void free_string(char *s); + +int new_client(const char *tenant, + bool update_periodically, + unsigned long update_frequency, + const char *hostname); + +void start_polling_update(const char *tenant); + +void free_client(struct Arc_Client *ptr); + +struct Arc_Client *get_client(const char *tenant); + +const char *cac_eval(struct Arc_Client *client, const char *query); + +const char *get_last_modified(struct Arc_Client *client); + +const char *get_config(struct Arc_Client *client); diff --git a/headers/libsuperposition_client.h b/headers/libsuperposition_client.h new file mode 100644 index 000000000..f37cbf580 --- /dev/null +++ b/headers/libsuperposition_client.h @@ -0,0 +1,26 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +typedef struct Arc_Client Arc_Client; + +int last_error_length(void); + +const char *last_error_message(void); + +void free_string(char *s); + +int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); + +void start_polling_update(const char *tenant); + +void free_client(struct Arc_Client *ptr); + +struct Arc_Client *get_client(const char *tenant); + +char *get_applicable_variant(struct Arc_Client *client, const char *c_context, short toss); + +char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); + +char *get_running_experiments(struct Arc_Client *client); diff --git a/libs b/libs new file mode 120000 index 000000000..6e0cd6fa6 --- /dev/null +++ b/libs @@ -0,0 +1 @@ +/nix/store/wap6n7b93hm0vnwn4kx8yafxb4yl7lwc-hs-cac-client-0.1.0.0 \ No newline at end of file From 0ce569b263986e9ad1d701480ae3c3b21eb7361c Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 2 Feb 2024 19:28:32 +0530 Subject: [PATCH 303/352] feat: haskell client for superposition Co-authored-by: Shivaraj B H <shivaraj-bh@users.noreply.github.com> --- Jenkinsfile | 1 + clients/haskell/cabal.project | 3 + clients/haskell/default.nix | 22 +++ .../{ => haskell}/hs-cac-client/CHANGELOG.md | 0 clients/{ => haskell}/hs-cac-client/LICENSE | 0 clients/{ => haskell}/hs-cac-client/README.md | 0 clients/{ => haskell}/hs-cac-client/Setup.hs | 0 .../hs-cac-client/hs-cac-client.cabal | 4 +- .../{ => haskell}/hs-cac-client/src/Client.hs | 0 .../{ => haskell}/hs-cac-client/src/Main.hs | 0 .../{ => haskell}/hs-exp-client/CHANGELOG.md | 0 clients/{ => haskell}/hs-exp-client/LICENSE | 0 clients/{ => haskell}/hs-exp-client/README.md | 0 clients/{ => haskell}/hs-exp-client/Setup.hs | 0 .../hs-exp-client/hs-exp-client.cabal | 6 +- .../{ => haskell}/hs-exp-client/src/Client.hs | 0 .../{ => haskell}/hs-exp-client/src/Main.hs | 0 clients/hs-cac-client/default.nix | 37 ----- clients/hs-exp-client/default.nix | 37 ----- crates/cac_client/build.rs | 6 +- crates/cac_client/default.nix | 63 -------- crates/cac_client/rust-toolchain.toml | 2 - crates/cac_client/src/interface.rs | 19 ++- .../experimentation-platform/src/db/schema.rs | 2 +- crates/superposition_client/build.rs | 6 +- crates/superposition_client/default.nix | 60 -------- .../superposition_client/rust-toolchain.toml | 2 - crates/superposition_client/src/interface.rs | 26 ++-- crates/superposition_client/src/lib.rs | 10 +- flake.lock | 74 ++++----- flake.nix | 141 ++++-------------- headers/libcac_client.h | 2 +- libs | 1 - rust-toolchain.toml | 4 + rust.nix | 71 +++++++++ 35 files changed, 219 insertions(+), 380 deletions(-) create mode 100644 clients/haskell/cabal.project create mode 100644 clients/haskell/default.nix rename clients/{ => haskell}/hs-cac-client/CHANGELOG.md (100%) rename clients/{ => haskell}/hs-cac-client/LICENSE (100%) rename clients/{ => haskell}/hs-cac-client/README.md (100%) rename clients/{ => haskell}/hs-cac-client/Setup.hs (100%) rename clients/{ => haskell}/hs-cac-client/hs-cac-client.cabal (97%) rename clients/{ => haskell}/hs-cac-client/src/Client.hs (100%) rename clients/{ => haskell}/hs-cac-client/src/Main.hs (100%) rename clients/{ => haskell}/hs-exp-client/CHANGELOG.md (100%) rename clients/{ => haskell}/hs-exp-client/LICENSE (100%) rename clients/{ => haskell}/hs-exp-client/README.md (100%) rename clients/{ => haskell}/hs-exp-client/Setup.hs (100%) rename clients/{ => haskell}/hs-exp-client/hs-exp-client.cabal (97%) rename clients/{ => haskell}/hs-exp-client/src/Client.hs (100%) rename clients/{ => haskell}/hs-exp-client/src/Main.hs (100%) delete mode 100644 clients/hs-cac-client/default.nix delete mode 100644 clients/hs-exp-client/default.nix delete mode 100644 crates/cac_client/default.nix delete mode 100644 crates/cac_client/rust-toolchain.toml delete mode 100644 crates/superposition_client/default.nix delete mode 100644 crates/superposition_client/rust-toolchain.toml delete mode 120000 libs create mode 100644 rust-toolchain.toml create mode 100644 rust.nix diff --git a/Jenkinsfile b/Jenkinsfile index 6d350d14c..afd28530a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,6 +59,7 @@ pipeline { sh 'git fetch' sh 'git config user.name ""Jenkins User""' sh 'git config user.email bitbucket.jenkins.read@juspay.in' + sh 'rustup toolchain uninstall stable && rustup toolchain install 1.73.0' } } } diff --git a/clients/haskell/cabal.project b/clients/haskell/cabal.project new file mode 100644 index 000000000..40050b05f --- /dev/null +++ b/clients/haskell/cabal.project @@ -0,0 +1,3 @@ +packages: + hs-cac-client + hs-exp-client diff --git a/clients/haskell/default.nix b/clients/haskell/default.nix new file mode 100644 index 000000000..ff3ea7d2b --- /dev/null +++ b/clients/haskell/default.nix @@ -0,0 +1,22 @@ +{ + perSystem = { config, pkgs, self', ... }: { + haskellProjects.default = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + cac_client.custom = _: self'.packages.superposition; + superposition_client.custom = _: self'.packages.superposition; + }; + }; + + devShells.haskell = pkgs.mkShell { + name = "superposition-haskell-clients"; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + ]; + shellHook = '' + export LIBRARY_PATH=${self'.packages.superposition}/lib + ''; + }; + }; +} diff --git a/clients/hs-cac-client/CHANGELOG.md b/clients/haskell/hs-cac-client/CHANGELOG.md similarity index 100% rename from clients/hs-cac-client/CHANGELOG.md rename to clients/haskell/hs-cac-client/CHANGELOG.md diff --git a/clients/hs-cac-client/LICENSE b/clients/haskell/hs-cac-client/LICENSE similarity index 100% rename from clients/hs-cac-client/LICENSE rename to clients/haskell/hs-cac-client/LICENSE diff --git a/clients/hs-cac-client/README.md b/clients/haskell/hs-cac-client/README.md similarity index 100% rename from clients/hs-cac-client/README.md rename to clients/haskell/hs-cac-client/README.md diff --git a/clients/hs-cac-client/Setup.hs b/clients/haskell/hs-cac-client/Setup.hs similarity index 100% rename from clients/hs-cac-client/Setup.hs rename to clients/haskell/hs-cac-client/Setup.hs diff --git a/clients/hs-cac-client/hs-cac-client.cabal b/clients/haskell/hs-cac-client/hs-cac-client.cabal similarity index 97% rename from clients/hs-cac-client/hs-cac-client.cabal rename to clients/haskell/hs-cac-client/hs-cac-client.cabal index 3d70ba9a0..3edc1c64a 100644 --- a/clients/hs-cac-client/hs-cac-client.cabal +++ b/clients/haskell/hs-cac-client/hs-cac-client.cabal @@ -63,6 +63,8 @@ library ViewPatterns BlockArguments ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + extra-libraries: + cac_client build-depends: aeson , base >=4.7 && <5 @@ -115,4 +117,4 @@ executable hs-exe , base >=4.7 && <5 , basic-prelude , hs-cac-client - default-language: Haskell2010 + default-language: Haskell2010 \ No newline at end of file diff --git a/clients/hs-cac-client/src/Client.hs b/clients/haskell/hs-cac-client/src/Client.hs similarity index 100% rename from clients/hs-cac-client/src/Client.hs rename to clients/haskell/hs-cac-client/src/Client.hs diff --git a/clients/hs-cac-client/src/Main.hs b/clients/haskell/hs-cac-client/src/Main.hs similarity index 100% rename from clients/hs-cac-client/src/Main.hs rename to clients/haskell/hs-cac-client/src/Main.hs diff --git a/clients/hs-exp-client/CHANGELOG.md b/clients/haskell/hs-exp-client/CHANGELOG.md similarity index 100% rename from clients/hs-exp-client/CHANGELOG.md rename to clients/haskell/hs-exp-client/CHANGELOG.md diff --git a/clients/hs-exp-client/LICENSE b/clients/haskell/hs-exp-client/LICENSE similarity index 100% rename from clients/hs-exp-client/LICENSE rename to clients/haskell/hs-exp-client/LICENSE diff --git a/clients/hs-exp-client/README.md b/clients/haskell/hs-exp-client/README.md similarity index 100% rename from clients/hs-exp-client/README.md rename to clients/haskell/hs-exp-client/README.md diff --git a/clients/hs-exp-client/Setup.hs b/clients/haskell/hs-exp-client/Setup.hs similarity index 100% rename from clients/hs-exp-client/Setup.hs rename to clients/haskell/hs-exp-client/Setup.hs diff --git a/clients/hs-exp-client/hs-exp-client.cabal b/clients/haskell/hs-exp-client/hs-exp-client.cabal similarity index 97% rename from clients/hs-exp-client/hs-exp-client.cabal rename to clients/haskell/hs-exp-client/hs-exp-client.cabal index cfa266e54..7fe61b7a9 100644 --- a/clients/hs-exp-client/hs-exp-client.cabal +++ b/clients/haskell/hs-exp-client/hs-exp-client.cabal @@ -25,7 +25,7 @@ source-repository head library exposed-modules: - + Client other-modules: Paths_hs_exp_client @@ -64,6 +64,8 @@ library ViewPatterns BlockArguments ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + extra-libraries: + superposition_client build-depends: aeson , base >=4.7 && <5 @@ -116,4 +118,4 @@ executable hs-exp-client-exe , base >=4.7 && <5 , basic-prelude , hs-exp-client - default-language: Haskell2010 + default-language: Haskell2010 \ No newline at end of file diff --git a/clients/hs-exp-client/src/Client.hs b/clients/haskell/hs-exp-client/src/Client.hs similarity index 100% rename from clients/hs-exp-client/src/Client.hs rename to clients/haskell/hs-exp-client/src/Client.hs diff --git a/clients/hs-exp-client/src/Main.hs b/clients/haskell/hs-exp-client/src/Main.hs similarity index 100% rename from clients/hs-exp-client/src/Main.hs rename to clients/haskell/hs-exp-client/src/Main.hs diff --git a/clients/hs-cac-client/default.nix b/clients/hs-cac-client/default.nix deleted file mode 100644 index b67cf868f..000000000 --- a/clients/hs-cac-client/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - perSystem = { config, pkgs, lib, self', ... }: { - haskellProjects.cac = { - projectRoot = ./.; - autoWire = [ "packages" "checks" "apps" ]; - settings = { - cac_client.custom = _: self'.packages.clients; - }; - }; - packages.hs-cac-client = self'.packages.cac-hs-cac-client.overrideAttrs (oa: { - cac_client = self'.packages.clients; - nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ - pkgs.makeWrapper - ]; - # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 - postFixup = (oa.postFixup or "") + '' - wrapProgram $out/bin/* \ - --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $cac_client/lib - ''; - }); - apps.cac.program = lib.getExe self'.packages.cac; - - devShells.cac-client = pkgs.mkShell { - name = "hs-cac-client"; - inputsFrom = [ - config.haskellProjects.default.outputs.devShell - config.flake-root.devShell - ]; - # TODO: set once, based on platform - # TODO(refactor): SRID: can we do this in one place? - # LD_LIBRARY_PATH = "${self'.packages.cac_client}/lib"; - shellHook = '' - export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" - ''; - }; - }; -} diff --git a/clients/hs-exp-client/default.nix b/clients/hs-exp-client/default.nix deleted file mode 100644 index 1c6136789..000000000 --- a/clients/hs-exp-client/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - perSystem = { config, pkgs, lib, self', ... }: { - haskellProjects.exp = { - projectRoot = ./.; - autoWire = [ "packages" "checks" "apps" ]; - settings = { - superposition_client.custom = _: self'.packages.clients; - }; - }; - packages.hs-exp-client = self'.packages.exp-hs-exp-client.overrideAttrs (oa: { - superposition_client = self'.packages.clients; - nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ - pkgs.makeWrapper - ]; - # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 - postFixup = (oa.postFixup or "") + '' - wrapProgram $out/bin/* \ - --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $superposition_client/lib - ''; - }); - apps.cac.program = lib.getExe self'.packages.cac; - - devShells.exp-client = pkgs.mkShell { - name = "hs-exp-client"; - inputsFrom = [ - config.haskellProjects.default.outputs.devShell - config.flake-root.devShell - ]; - # TODO: set once, based on platform - # TODO(refactor): SRID: can we do this in one place? - # LD_LIBRARY_PATH = "${self'.packages.superposition_client}/lib"; - shellHook = '' - export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" - ''; - }; - }; -} diff --git a/crates/cac_client/build.rs b/crates/cac_client/build.rs index b1eb06cc8..cb9d3c4d2 100644 --- a/crates/cac_client/build.rs +++ b/crates/cac_client/build.rs @@ -6,6 +6,6 @@ fn main() { config.language = cbindgen::Language::C; println!("Calling build.rs in cac_client"); cbindgen::generate_with_config(&crate_dir, config) - .unwrap() - .write_to_file("../../headers/libcac_client.h"); -} \ No newline at end of file + .unwrap() + .write_to_file("../../headers/libcac_client.h"); +} diff --git a/crates/cac_client/default.nix b/crates/cac_client/default.nix deleted file mode 100644 index b0c2af779..000000000 --- a/crates/cac_client/default.nix +++ /dev/null @@ -1,63 +0,0 @@ -# Nix for Rust project management -{ inputs, ... }: { - perSystem = { config, self', pkgs, lib, system, ... }: - let - rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = [ - "rust-src" - "rust-analyzer" - "clippy" - ]; - }; - craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; - args = { - pname = "cac_client"; - src = ./.; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cargoVendorDir = craneLib.vendorCargoDeps { - src = ./../../.; - }; - cargoArtifacts = craneLib.buildDepsOnly args; - package = craneLib.buildPackage (args // { - inherit cargoArtifacts; - doCheck = false; # FIXME: tests require services to be running - }); - - check = craneLib.cargoClippy (args // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; - }); - in - { - packages.cac_client = package; - - checks.clippy = check; - - # # Flake outputs - # devShells.rust-exp = pkgs.mkShell { - # inputsFrom = [ - # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) - # ]; - # shellHook = '' - # # For rust-analyzer 'hover' tooltips to work. - # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; - # ''; - # nativeBuildInputs = with pkgs; [ - # # Add your dev tools here. - # rustToolchain - # cargo-watch - # ]; - # }; - }; -} diff --git a/crates/cac_client/rust-toolchain.toml b/crates/cac_client/rust-toolchain.toml deleted file mode 100644 index 31578d3bf..000000000 --- a/crates/cac_client/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" \ No newline at end of file diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs index f79a1cffc..eaffee304 100644 --- a/crates/cac_client/src/interface.rs +++ b/crates/cac_client/src/interface.rs @@ -5,11 +5,12 @@ use std::{ sync::Arc, }; -use crate::{utils::core::MapError, Client, CLIENT_FACTORY}; +use crate::{utils::core::MapError, Client, MergeStrategy, CLIENT_FACTORY}; use serde_json::{Map, Value}; use std::{ cell::RefCell, ffi::{c_int, CString}, + str::FromStr, time::Duration, }; use tokio::{runtime::Runtime, task}; @@ -160,6 +161,7 @@ pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc<Client> { pub extern "C" fn cac_eval( client: *mut Arc<Client>, query: *const c_char, + merge_strategy: *const c_char, ) -> *const c_char { let context_string = match cstring_to_rstring(query) { Ok(s) => s, @@ -176,7 +178,20 @@ pub extern "C" fn cac_eval( return std::ptr::null(); } }; - let overrides = match unsafe { (*client).eval(context) } { + let merge_strategy = match cstring_to_rstring(merge_strategy) { + Ok(s) => match MergeStrategy::from_str(s.as_str()) { + Ok(strat) => strat, + Err(err) => { + update_last_error(err.to_string()); + return std::ptr::null(); + } + }, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + let overrides = match unsafe { (*client).eval(context, merge_strategy) } { Ok(ov) => match serde_json::to_string::<Map<String, Value>>(&ov) { Ok(ove) => ove, Err(err) => { diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 6cd391ae0..a8676f480 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -616,4 +616,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m11, event_log_y2026m12, experiments, -); \ No newline at end of file +); diff --git a/crates/superposition_client/build.rs b/crates/superposition_client/build.rs index e72f5872b..707b90a57 100644 --- a/crates/superposition_client/build.rs +++ b/crates/superposition_client/build.rs @@ -5,6 +5,6 @@ fn main() { let mut config: cbindgen::Config = Default::default(); config.language = cbindgen::Language::C; cbindgen::generate_with_config(&crate_dir, config) - .unwrap() - .write_to_file("../../headers/libsuperposition_client.h"); -} \ No newline at end of file + .unwrap() + .write_to_file("../../headers/libsuperposition_client.h"); +} diff --git a/crates/superposition_client/default.nix b/crates/superposition_client/default.nix deleted file mode 100644 index a78ee5e5d..000000000 --- a/crates/superposition_client/default.nix +++ /dev/null @@ -1,60 +0,0 @@ -# Nix for Rust project management -{ inputs, ... }: { - perSystem = { config, self', pkgs, lib, system, ... }: - let - rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = [ - "rust-src" - "rust-analyzer" - "clippy" - ]; - }; - craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; - args = { - pname = "superposition_client"; - src = ./.; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cargoArtifacts = craneLib.buildDepsOnly args; - package = craneLib.buildPackage (args // { - inherit cargoArtifacts; - doCheck = false; # FIXME: tests require services to be running - }); - - check = craneLib.cargoClippy (args // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; - }); - in - { - packages.superposition_client = package; - - checks.clippy = check; - - # # Flake outputs - # devShells.rust-exp = pkgs.mkShell { - # inputsFrom = [ - # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) - # ]; - # shellHook = '' - # # For rust-analyzer 'hover' tooltips to work. - # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; - # ''; - # nativeBuildInputs = with pkgs; [ - # # Add your dev tools here. - # rustToolchain - # cargo-watch - # ]; - # }; - }; -} diff --git a/crates/superposition_client/rust-toolchain.toml b/crates/superposition_client/rust-toolchain.toml deleted file mode 100644 index 31578d3bf..000000000 --- a/crates/superposition_client/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" \ No newline at end of file diff --git a/crates/superposition_client/src/interface.rs b/crates/superposition_client/src/interface.rs index 7ace9d349..3ec05345a 100644 --- a/crates/superposition_client/src/interface.rs +++ b/crates/superposition_client/src/interface.rs @@ -1,4 +1,3 @@ -#[warn(unused_assignments)] use std::{ ffi::{c_char, c_ulong, CStr}, sync::Arc, @@ -13,7 +12,7 @@ use std::{ use tokio::{runtime::Runtime, task}; thread_local! { - static LAST_ERROR: RefCell<Option<Box<String>>> = RefCell::new(None); + static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None); } fn to_string<E>(e: E) -> String @@ -25,7 +24,7 @@ where fn error_block<E>(err: String) -> *mut E { update_last_error(err); - return std::ptr::null_mut(); + std::ptr::null_mut() } fn cstring_to_rstring(s: *const c_char) -> Result<String, String> { @@ -41,12 +40,12 @@ pub fn update_last_error(err: String) { println!("Setting LAST_ERROR: {}", err); LAST_ERROR.with(|prev| { - *prev.borrow_mut() = Some(Box::new(err)); + *prev.borrow_mut() = Some(err); }); } -pub fn take_last_error() -> Option<Box<String>> { - LAST_ERROR.with(|prev| prev.borrow_mut().take()) +pub fn take_last_error() -> Option<String> { + LAST_ERROR.with(|prev| prev.take()) } #[no_mangle] @@ -85,7 +84,6 @@ pub extern "C" fn new_client( update_frequency: c_ulong, hostname: *const c_char, ) -> c_int { - let duration = update_frequency as u64; let tenant = match cstring_to_rstring(tenant) { Ok(value) => value, Err(err) => { @@ -105,17 +103,17 @@ pub extern "C" fn new_client( let local = task::LocalSet::new(); local.block_on(&Runtime::new().unwrap(), async move { match CLIENT_FACTORY - .create_client(tenant.clone(), duration, hostname) + .create_client(tenant.clone(), update_frequency, hostname) .await { - Ok(_) => return 0, + Ok(_) => 0, Err(err) => { update_last_error(err); - return 1; + 1 } } }); - return 0; + 0 } #[no_mangle] @@ -192,7 +190,7 @@ pub extern "C" fn get_applicable_variant( // println!("variantIds: {:?}", variants); match serde_json::to_string::<Vec<String>>(&variants) { Ok(result) => rstring_to_cstring(result).into_raw(), - Err(err) => return error_block(err.to_string()), + Err(err) => error_block(err.to_string()), } } @@ -219,7 +217,7 @@ pub extern "C" fn get_satisfied_experiments( }; match serde_json::to_string(&experiments) { Ok(result) => rstring_to_cstring(result).into_raw(), - Err(err) => return error_block(err.to_string()), + Err(err) => error_block(err.to_string()), } } @@ -235,6 +233,6 @@ pub extern "C" fn get_running_experiments(client: *mut Arc<Client>) -> *mut c_ch }; match serde_json::to_string(&experiments) { Ok(result) => rstring_to_cstring(result).into_raw(), - Err(err) => return error_block(err.to_string()), + Err(err) => error_block(err.to_string()), } } diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index 9c58a59bf..a71143021 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -1,5 +1,5 @@ -mod types; mod interface; +mod types; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; @@ -30,7 +30,7 @@ impl Client { experiments: Arc::new(RwLock::new(HashMap::new())), http_client: reqwest::Client::new(), last_polled: Arc::new(RwLock::new( - Utc.with_ymd_and_hms(2023, 01, 01, 0, 0, 0).unwrap(), + Utc.with_ymd_and_hms(2023, 01, 1, 0, 0, 0).unwrap(), )), } } @@ -183,12 +183,12 @@ impl ClientFactory { let client = Arc::new(Client::new(Config { tenant: tenant.to_string(), - hostname: hostname, - poll_frequency: poll_frequency, + hostname, + poll_frequency, })); factory.insert(tenant.to_string(), client.clone()); - return Ok(client.clone()); + Ok(client.clone()) } pub async fn get_client(&self, tenant: String) -> Result<Arc<Client>, String> { diff --git a/flake.lock b/flake.lock index 2aea8b76d..02944e91f 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1705974079, - "narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=", + "lastModified": 1707461758, + "narHash": "sha256-VaqINICYEtVKF0X+chdNtXcNp6poZr385v6AG7j0ybM=", "owner": "ipetkov", "repo": "crane", - "rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2", + "rev": "505976eaeac289fe41d074bee37006ac094636bb", "type": "github" }, "original": { @@ -25,11 +25,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1704982712, - "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", + "lastModified": 1706830856, + "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "07f6395285469419cf9d078f59b5b49993198c00", + "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", "type": "github" }, "original": { @@ -58,11 +58,11 @@ }, "haskell-flake": { "locked": { - "lastModified": 1705984248, - "narHash": "sha256-1zW2udPF4clKCuTgk0FHjGUB7x/Tfe8m9oUhe3hM/YQ=", + "lastModified": 1707242163, + "narHash": "sha256-w+cBynh7yqnpVtFdu1SEZxPgtlz/nWnv47D5crnPXHM=", "owner": "srid", "repo": "haskell-flake", - "rev": "cea3332b027264e914673df392870ebb1e92de44", + "rev": "f9d17c3aa68e65529f424816c8b9346ae602d1de", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1706550542, - "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "lastModified": 1707268954, + "narHash": "sha256-2en1kvde3cJVc3ZnTy8QeD2oKcseLFjYPLKhIGDanQ0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1", "type": "github" }, "original": { @@ -90,11 +90,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1703961334, - "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", + "lastModified": 1706550542, + "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", + "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", "type": "github" }, "original": { @@ -105,42 +105,29 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1706487304, - "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "crane": "crane", "flake-parts": "flake-parts", "haskell-flake": "haskell-flake", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "rust-overlay": "rust-overlay", + "systems": "systems_2" } }, "rust-overlay": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1707790272, - "narHash": "sha256-KQXPNl3BLdRbz7xx+mwIq/017fxLRk6JhXHxVWCKsTU=", + "lastModified": 1707444620, + "narHash": "sha256-P8kRkiJLFttN+hbAOlm11wPxUrQZqKle+QtVCqFiGXY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8dfbe2dffc28c1a18a29ffa34d5d0b269622b158", + "rev": "78503e9199010a4df714f29a4f9c00eb2ccae071", "type": "github" }, "original": { @@ -163,6 +150,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 9ead5676c..fa16e1000 100644 --- a/flake.nix +++ b/flake.nix @@ -1,124 +1,45 @@ { inputs = { - flake-parts.url = "github:hercules-ci/flake-parts"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - crane.url = "github:ipetkov/crane"; - crane.inputs.nixpkgs.follows = "nixpkgs"; + flake-parts.url = "github:hercules-ci/flake-parts"; haskell-flake.url = "github:srid/haskell-flake"; - rust-overlay = { url = "github:oxalica/rust-overlay"; }; + systems.url = "github:nix-systems/default"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = inputs@{ self, rust-overlay, flake-parts, crane, nixpkgs, haskell-flake, ... }: + outputs = inputs: - flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ]; + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; imports = [ inputs.haskell-flake.flakeModule - ./clients/hs-cac-client - ./clients/hs-exp-client + ./clients/haskell + ./rust.nix ]; - perSystem = { config, self', inputs', system, pkgs, lib, haskell-flake, ... }: - let - craneLib = crane.lib.${system}; - cac_args = { - pname = "cac_client"; - src = ./crates/cac_client; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - exp_args = { - pname = "superposition_client"; - src = ./crates/superposition_client; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cacCargoArtifacts = craneLib.buildDepsOnly cac_args; - expCargoArtifacts = craneLib.buildDepsOnly exp_args; - cac_client = craneLib.buildPackage (cac_args // { - inherit cacCargoArtifacts; - }); - superposition_client = craneLib.buildPackage (exp_args // { - inherit expCargoArtifacts; - }); - in - rec { - _module.args.pkgs = import nixpkgs { - inherit system; - overlays = [ rust-overlay.overlay ]; - - }; - formatter = pkgs.nixpkgs-fmt; - # For `nix build` & `nix run`: - - packages.clients = craneLib.buildPackage { - nativeBuildInputs = with pkgs; [ postgresql_12 iconv darwin.apple_sdk.frameworks.Security ]; - src = craneLib.path ./.; # FIXME: move rust stuff to subdir - }; - - # For `nix develop`: - devShells.default = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - # bring your local shell properties into nix env - # shellHook = '' - # echo "you are now in the nix shell" - # eval $($SHELL) - # ''; - nativeBuildInputs = - let - univPkgs = with pkgs; [ - # Build requirements - cargo - libiconv - openssl - postgresql_12 - # Extras - rust-analyzer - rustfmt - bacon - cargo-watch - clippy - diesel-cli - docker-compose - stdenv.cc - pkg-config - awscli - jq - pkgs.nodejs_18 - leptosfmt - wasm-pack - curl - ( rust-bin.stable."1.73.0".default.override { - extensions = [ "rust-src" ]; - targets = [ "wasm32-unknown-unknown" ]; - }) - ]; - darwinPkgs = with pkgs; [ - darwin.apple_sdk.frameworks.Security - darwin.apple_sdk.frameworks.SystemConfiguration - ]; - in - univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else [ ]); - }; + perSystem = { pkgs, self', ... }: { + devShells.default = pkgs.mkShell { + inputsFrom = [ + self'.devShells.rust + ]; + packages = with pkgs; [ + docker-compose + # Why do we need this? + stdenv.cc + awscli + jq + nodejs_18 + nixpkgs-fmt + ]; }; + }; }; -} +} \ No newline at end of file diff --git a/headers/libcac_client.h b/headers/libcac_client.h index cf6ef0606..07141a7a9 100644 --- a/headers/libcac_client.h +++ b/headers/libcac_client.h @@ -22,7 +22,7 @@ void free_client(struct Arc_Client *ptr); struct Arc_Client *get_client(const char *tenant); -const char *cac_eval(struct Arc_Client *client, const char *query); +const char *cac_eval(struct Arc_Client *client, const char *query, const char *merge_strategy); const char *get_last_modified(struct Arc_Client *client); diff --git a/libs b/libs deleted file mode 120000 index 6e0cd6fa6..000000000 --- a/libs +++ /dev/null @@ -1 +0,0 @@ -/nix/store/wap6n7b93hm0vnwn4kx8yafxb4yl7lwc-hs-cac-client-0.1.0.0 \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..3fe7414d2 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +targets = ["wasm32-unknown-unknown"] +components = ["rust-src", "rustfmt", "clippy", "rust-analyzer"] diff --git a/rust.nix b/rust.nix new file mode 100644 index 000000000..11b179860 --- /dev/null +++ b/rust.nix @@ -0,0 +1,71 @@ +{ inputs, ... }: +{ + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + craneArgs = { + pname = "superposition"; + version = "0.0.1"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + pkgs.fixDarwinDylibNames + ]) ++ [ + pkgs.libiconv + pkgs.openssl + pkgs.postgresql_12 + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly craneArgs; + package = craneLib.buildPackage (craneArgs // { + inherit cargoArtifacts; + # https://discourse.nixos.org/t/how-to-use-install-name-tool-on-darwin/9931/2 + postInstall = '' + ${if pkgs.stdenv.isDarwin then "fixDarwinDylibNames" else ""} + ''; + }); + + check = craneLib.cargoClippy (craneArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ inputs.rust-overlay.overlays.default ]; + + }; + + packages.superposition = package; + + checks.clippy = check; + + # Flake outputs + devShells.rust = pkgs.mkShell { + inputsFrom = [ + package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + ]; + shellHook = '' + # For rust-analyzer 'hover' tooltips to work. + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + ''; + nativeBuildInputs = with pkgs; [ + # Add your dev tools here. + bacon + cargo-watch + diesel-cli + leptosfmt + rustToolchain + wasm-pack + ] ++ craneArgs.nativeBuildInputs; + }; + }; +} \ No newline at end of file From 7067b36691886fcb37a265d23598e11c2d17b404 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Fri, 15 Mar 2024 16:10:06 +0530 Subject: [PATCH 304/352] fix: moved to AWS Public ECR for docker images --- Dockerfile | 4 ++-- docker-compose/localstack/Dockerfile | 4 ++-- docker-compose/postgres/Dockerfile | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c542dabd..57c11e912 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.73 as builder +FROM public.ecr.aws/docker/library/rust:1.73.0 as builder WORKDIR /build ENV NVM_DIR /usr/local/nvm ENV NODE_VERSION 18.19.0 @@ -36,7 +36,7 @@ RUN cp .env.example .env # building backend RUN --mount=type=ssh cargo build --release -FROM debian:bookworm-slim +FROM public.ecr.aws/debian/debian:bookworm-slim WORKDIR /app ENV NODE_VERSION=18.19.0 diff --git a/docker-compose/localstack/Dockerfile b/docker-compose/localstack/Dockerfile index 41570683e..e4b84e18b 100644 --- a/docker-compose/localstack/Dockerfile +++ b/docker-compose/localstack/Dockerfile @@ -1,5 +1,5 @@ -FROM localstack/localstack:1.3.0 +FROM public.ecr.aws/localstack/localstack:1.3.0 RUN aws configure set aws_access_key_id test RUN aws configure set aws_secret_access_key test -RUN aws configure set default.region ap-south-1 +RUN aws configure set default.region ap-south-1 \ No newline at end of file diff --git a/docker-compose/postgres/Dockerfile b/docker-compose/postgres/Dockerfile index 303df06f8..bfabfe38b 100644 --- a/docker-compose/postgres/Dockerfile +++ b/docker-compose/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:12-alpine +FROM public.ecr.aws/docker/library/postgres:12-alpine COPY ./db_init.sql /docker-entrypoint-initdb.d/db_init.sql -# CMD ["postgres", "-c", "log_statement=all"] +# CMD ["postgres", "-c", "log_statement=all"] \ No newline at end of file From 6897659e59b4fdfb2bff387837f0cf5a160d2ae9 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Mon, 18 Mar 2024 12:20:29 +0530 Subject: [PATCH 305/352] fix: update cargo.lock --- Cargo.lock | 6 +++--- Dockerfile | 2 +- Jenkinsfile | 2 +- rust-toolchain.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab475ae79..7ec47ebaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,7 +665,7 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3", - "clap", + "clap 4.3.4", "derive_more", "env_logger 0.8.4", "evalexpr", @@ -700,7 +700,7 @@ dependencies = [ "serde_json", "syn 1.0.109", "tempfile", - "toml", + "toml 0.5.11", ] [[package]] @@ -4015,7 +4015,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", diff --git a/Dockerfile b/Dockerfile index 57c11e912..c3b0df1f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/rust:1.73.0 as builder +FROM public.ecr.aws/docker/library/rust:1.76.0 as builder WORKDIR /build ENV NVM_DIR /usr/local/nvm ENV NODE_VERSION 18.19.0 diff --git a/Jenkinsfile b/Jenkinsfile index afd28530a..d60fd42ca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ pipeline { sh 'git fetch' sh 'git config user.name ""Jenkins User""' sh 'git config user.email bitbucket.jenkins.read@juspay.in' - sh 'rustup toolchain uninstall stable && rustup toolchain install 1.73.0' + sh 'rustup toolchain uninstall stable && rustup toolchain install 1.76.0' } } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3fe7414d2..5ac7f874c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" +channel = "1.76.0" targets = ["wasm32-unknown-unknown"] components = ["rust-src", "rustfmt", "clippy", "rust-analyzer"] From ae880834d9a5cf5bdd01c7c6efa5775914b37640 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 18 Mar 2024 09:12:18 +0000 Subject: [PATCH 306/352] chore(version): v0.32.0 [skip ci] --- CHANGELOG.md | 16 ++++++++++++++++ Cargo.lock | 6 +++--- crates/cac_client/CHANGELOG.md | 8 ++++++++ crates/cac_client/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 8 ++++++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/superposition_client/CHANGELOG.md | 8 ++++++++ crates/superposition_client/Cargo.toml | 4 ++-- 8 files changed, 47 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc1cab38..1d0890fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.32.0 - 2024-03-18 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.11.0 +- cac_client bumped to cac_client-v0.7.0 +- superposition_client bumped to superposition_client-v0.5.0 +### Global changes +#### Bug Fixes +- [PICAF-26428] update cargo.lock - (b7aa8b6) - Kartik +- [PICAF-26428] moved to AWS Public ECR for docker images - (26cd710) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + ## v0.31.0 - 2024-03-08 ### Package updates - external bumped to external-v0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 7ec47ebaa..7412481b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.6.0" +version = "0.7.0" dependencies = [ "actix-web", "cbindgen", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.10.0" +version = "0.11.0" dependencies = [ "actix", "actix-web", @@ -3696,7 +3696,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superposition_client" -version = "0.4.0" +version = "0.5.0" dependencies = [ "cbindgen", "chrono", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 49158026c..3eb485470 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.7.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + ## cac_client-v0.6.0 - 2024-03-04 #### Features - PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index ae6a8666a..ff7ef954c 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.6.0" +version = "0.7.0" edition = "2021" build = "build.rs" diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index f9e2f14c2..f8cd72cbb 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.11.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + ## experimentation-platform-v0.10.0 - 2024-03-08 #### Features - PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index 5c33e1a4f..ed31dd262 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.10.0" +version = "0.11.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/superposition_client/CHANGELOG.md b/crates/superposition_client/CHANGELOG.md index 77dc81b12..72d80be3a 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/superposition_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## superposition_client-v0.5.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + ## superposition_client-v0.4.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman diff --git a/crates/superposition_client/Cargo.toml b/crates/superposition_client/Cargo.toml index bec562de6..ecb21cb3f 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/superposition_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "superposition_client" -version = "0.4.0" +version = "0.5.0" edition = "2021" [dependencies] @@ -20,4 +20,4 @@ name = "superposition_client" crate-type = ["cdylib", "lib"] [build-dependencies] -cbindgen = "0.26.0" \ No newline at end of file +cbindgen = "0.26.0" From 290b32610a59d1fbb44faf38e62e9083e6dd8cc5 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Wed, 13 Mar 2024 15:58:40 +0530 Subject: [PATCH 307/352] fix: added routes without service prefix for b/w compatibility --- crates/context-aware-config/src/main.rs | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index b1e1142d8..eb1567333 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -165,6 +165,7 @@ async fn main() -> Result<()> { let cac_host = cac_host.to_owned() + base.as_str(); App::new() .wrap(DashboardAuth::default(authenticated_routes(base.as_str()))) + .wrap(DashboardAuth::default(authenticated_routes(""))) .wrap(TenantMiddlewareFactory) .wrap(middlewares::cors()) .wrap(GoldenSignalFactory) @@ -261,6 +262,47 @@ async fn main() -> Result<()> { .service(Files::new("/assets", format!("{site_root}"))) // serve the favicon from /favicon.ico ) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AuditHeader::new(TableName::Contexts)) + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) + .service( + external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) .app_data(Data::new(leptos_options.to_owned())) }) .bind(("0.0.0.0", 8080))? From b177bc9d34a514abd7ffcd67e91accbbe34db12d Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Tue, 19 Mar 2024 08:08:03 +0000 Subject: [PATCH 308/352] chore(version): v0.32.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 8 ++++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0890fea..7ea752b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.32.1 - 2024-03-19 +### Package updates +- context-aware-config bumped to context-aware-config-v0.23.1 +### Global changes + +- - - + ## v0.32.0 - 2024-03-18 ### Package updates - experimentation-platform bumped to experimentation-platform-v0.11.0 diff --git a/Cargo.lock b/Cargo.lock index 7412481b0..5c81d9096 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.23.0" +version = "0.23.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 267d34033..96a67cad4 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.23.1 - 2024-03-19 +#### Bug Fixes +- [PICAF-26348] added routes without service prefix for b/w compatibility - (079c02d) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan + +- - - + ## context-aware-config-v0.23.0 - 2024-03-08 #### Features - PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index b7deae705..3c92193d3 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.23.0" +version = "0.23.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From d868254e233c0131a1f018e81f9c69ee1a441eb0 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Thu, 14 Mar 2024 18:06:08 +0530 Subject: [PATCH 309/352] fix: auto-create variantIds dimension --- scripts/create-tenant.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index c912e434c..fb3d48f11 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -34,5 +34,6 @@ function generate_sql() { generate_sql "context-aware-config" $CAC_SCHEMA generate_sql "experimentation-platform" $EXP_SCHEMA +psql "$DB_URL" -c "INSERT INTO $CAC_SCHEMA.dimensions (dimension, priority, created_at, created_by, schema, function_name) VALUES ('variantIds', 1, CURRENT_TIMESTAMP, 'anon@juspay.in', '{\"type\": \"string\",\"pattern\": \".*\"}'::json, null);" shopt -u extglob \ No newline at end of file From 396c68d08ba16e1450423a4b89188ace9c88b68a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Thu, 14 Mar 2024 12:11:08 +0530 Subject: [PATCH 310/352] fix: Functions bug fixes --- .../src/api/context/helpers.rs | 2 +- .../src/api/default_config/handlers.rs | 67 +++++++++++++------ .../src/api/default_config/types.rs | 3 +- .../src/api/dimension/handlers.rs | 31 ++++++--- .../src/api/dimension/types.rs | 15 ++++- .../src/api/functions/handlers.rs | 4 +- crates/context-aware-config/src/db/models.rs | 2 + .../src/validation_functions.rs | 9 +-- .../context-aware-config/tests/cac_tests.rs | 14 ++-- 9 files changed, 101 insertions(+), 46 deletions(-) diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context-aware-config/src/api/context/helpers.rs index 327eb4ea4..57de21203 100644 --- a/crates/context-aware-config/src/api/context/helpers.rs +++ b/crates/context-aware-config/src/api/context/helpers.rs @@ -119,7 +119,7 @@ fn validate_value_with_function( if let Err((e, stdout)) = execute_fn(&utf8_decoded, fun_name, value.to_owned()) { log::error!("function validation failed for {key} with error: {e}"); return Err(anyhow!(json!({ - "message": format!("function validation failed for {}", key), "stdout": stdout + "message": format!("function validation failed for {} with error {}", key, e), "stdout": stdout }))); } Ok(()) diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 54fc05b98..8c8438cbb 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -51,28 +51,55 @@ async fn create( }))); } - let default_config = if req.value.is_none() || req.schema.is_none() { - let (value, schema, function_name) = fetch_default_key(&key, &mut conn) - .map_err(|e| ErrorBadRequest(json!({"message" : e.to_string()})))?; - DefaultConfig { - key: key.to_owned(), - value: req.value.unwrap_or_else(|| value), - schema: req.schema.map_or_else(|| schema, Value::Object), - function_name: req.function_name.or(function_name), - created_by: user.email, - created_at: Utc::now(), + let func_name = match &req.function_name { + Some(Value::String(s)) => Some(s.clone()), + Some(Value::Null) | None => None, + Some(_) => { + return Err(ErrorBadRequest(json!({ + "message": "Expected a string or null as the function name." + }))) } - } else { - DefaultConfig { - key: key.to_owned(), - value: req.value.unwrap(), - schema: Value::Object(req.schema.unwrap()), - function_name: req.function_name, - created_by: user.email, - created_at: Utc::now(), + }; + + let result = fetch_default_key(&key, &mut conn); + + let (value, schema, function_name) = match result { + Ok((val, schema, f_name)) => { + let val = req.value.unwrap_or_else(|| val); + let schema = req.schema.map_or_else(|| schema, Value::Object); + let f_name = if req.function_name == Some(Value::Null) { + None + } else { + func_name.or(f_name) + }; + (val, schema, f_name) + } + Err(diesel::NotFound) => match (req.value, req.schema) { + (Some(val), Some(schema)) => (val, Value::Object(schema), func_name), + _ => { + log::error!("No record found for {key}."); + return Err(ErrorBadRequest(json!({ + "message": format!( "No record found for {key}." ) + }))); + } + }, + Err(e) => { + log::error!("Failed to fetch default_config {key} with error: {e}."); + return Err(ErrorBadRequest(json!({ + "message": format!( "Something went wrong." ) + }))); } }; + let default_config = DefaultConfig { + key: key.to_owned(), + value, + schema, + function_name, + created_by: user.email, + created_at: Utc::now(), + }; + if let Err(e) = validate_jsonschema( &state.default_config_validation_schema, &default_config.schema, @@ -120,7 +147,7 @@ async fn create( { log::info!("function validation failed for {key} with error: {e}"); return Err(ErrorBadRequest(json!({ - "message": "function validation failed", "stdout": stdout + "message": format!( "function validation failed with error: {}", e), "stdout": stdout }))); } } @@ -149,7 +176,7 @@ async fn create( fn fetch_default_key( key: &String, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> anyhow::Result<(Value, Value, Option<String>)> { +) -> Result<(Value, Value, Option<String>), diesel::result::Error> { let res: (Value, Value, Option<String>) = default_configs .filter(db::schema::default_configs::key.eq(key)) .select(( diff --git a/crates/context-aware-config/src/api/default_config/types.rs b/crates/context-aware-config/src/api/default_config/types.rs index b6a53528d..0e4b1b615 100644 --- a/crates/context-aware-config/src/api/default_config/types.rs +++ b/crates/context-aware-config/src/api/default_config/types.rs @@ -6,7 +6,8 @@ pub struct CreateReq { #[serde(default, deserialize_with = "deserialize_option")] pub value: Option<Value>, pub schema: Option<Map<String, Value>>, - pub function_name: Option<String>, + #[serde(default, deserialize_with = "deserialize_option")] + pub function_name: Option<Value>, } fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index e6b04fd4b..fc86716e4 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -1,5 +1,5 @@ use crate::{ - api::{dimension::types::CreateReq, functions::helpers::fetch_function}, + api::dimension::types::CreateReq, db::{models::Dimension, schema::dimensions::dsl::*}, helpers::validate_jsonschema, }; @@ -12,6 +12,7 @@ use chrono::Utc; use dashboard_auth::types::User; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; +use serde_json::Value; use service_utils::{ service::types::{AppState, DbConnection}, types as app, @@ -51,14 +52,16 @@ async fn create( .body(String::from(format!("Bad schema: {:?}", e))); }; - if let Some(func_name) = create_req.function_name.clone() { - let function = fetch_function(&func_name, &mut conn); - if let Err(e) = function { - log::error!("{func_name} function not found with error: {e:?}"); - return HttpResponse::BadRequest() - .body(String::from(format!("{func_name} function not found"))); + let fun_name = match create_req.function_name { + Some(Value::String(func_name)) => Some(func_name), + Some(Value::Null) | None => None, + _ => { + log::error!("Expected a string or null as the function name."); + return HttpResponse::BadRequest().body(String::from(format!( + "Expected a string or null as the function name." + ))); } - } + }; let new_dimension = Dimension { dimension: create_req.dimension, @@ -66,7 +69,7 @@ async fn create( schema: schema_value, created_by: user.email, created_at: Utc::now(), - function_name: create_req.function_name, + function_name: fun_name.clone(), }; let upsert = diesel::insert_into(dimensions) @@ -81,8 +84,16 @@ async fn create( return HttpResponse::Created() .body("Dimension created/updated successfully.") } + Err(diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::ForeignKeyViolation, + e, + )) => { + log::error!("{fun_name:?} function not found with error: {e:?}"); + return HttpResponse::BadRequest() + .body(String::from(format!("Function not found."))); + } Err(e) => { - log::info!("Dimension upsert failed with error: {e}"); + log::error!("Dimension upsert failed with error: {e}"); return HttpResponse::InternalServerError() .body("Failed to create/update dimension\n"); } diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context-aware-config/src/api/dimension/types.rs index 572bf21ff..1c81a69e1 100644 --- a/crates/context-aware-config/src/api/dimension/types.rs +++ b/crates/context-aware-config/src/api/dimension/types.rs @@ -1,10 +1,19 @@ -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use serde_json::Value; -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct CreateReq { pub dimension: String, pub priority: u16, pub schema: Value, - pub function_name: Option<String>, + #[serde(default, deserialize_with = "deserialize_option")] + pub function_name: Option<Value>, +} + +fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(Some(value)) } diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index ce8e901d2..d71f3a389 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -48,7 +48,7 @@ async fn create( let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - if let Err(e) = compile_fn(&req.function) { + if let Err(e) = compile_fn(&req.function, &req.function_name) { return Err(ErrorBadRequest(json!({ "message": e }))); } @@ -127,7 +127,7 @@ async fn update( // Function Linter Check if let Some(function) = &req.function { - if let Err(e) = compile_fn(function) { + if let Err(e) = compile_fn(function, &f_name) { return Err(ErrorBadRequest(json!({ "message": e }))); } } diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context-aware-config/src/db/models.rs index b4362c348..86e275522 100644 --- a/crates/context-aware-config/src/db/models.rs +++ b/crates/context-aware-config/src/db/models.rs @@ -21,6 +21,7 @@ pub struct Context { #[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(dimension))] +#[diesel(treat_none_as_null = true)] pub struct Dimension { pub dimension: String, pub priority: i32, @@ -33,6 +34,7 @@ pub struct Dimension { #[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone)] #[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(primary_key(key))] +#[diesel(treat_none_as_null = true)] pub struct DefaultConfig { pub key: String, pub value: Value, diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 1e784fe51..dd5993ab0 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -92,16 +92,17 @@ pub fn execute_fn( } } -fn eslint_logic(code_str: &str) -> String { - let code = IMPORT_CODE.to_string() + code_str; +fn eslint_logic(code_str: &str, fun_name: &str) -> String { + let code = + IMPORT_CODE.to_string() + code_str + &format!("console.log({});", fun_name); let fun_call: String = format!("\nconst codeToLint = {};", json!(code)); fun_call + ES_LINT_CODE } -pub fn compile_fn(code_str: &str) -> Result<(), String> { +pub fn compile_fn(code_str: &str, fun_name: &str) -> Result<(), String> { let output = Command::new("node") .arg("-e") - .arg(eslint_logic(code_str)) + .arg(eslint_logic(code_str, fun_name)) .output(); log::trace!("{}", format!("validation function output : {:?}", output)); match output { diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index eeab38e4f..f41f30b92 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -29,15 +29,19 @@ fn test_execute_fn() { Ok(_) => false, Err((e, stdout)) => e.contains("Bad schema"), }; - let err_compile = match compile_fn(&(compile_code_error.to_owned())) { - Ok(()) => false, - Err(e) => e.contains("Bad schema"), - }; + let err_compile = + match compile_fn(&(compile_code_error.to_owned()), &"test_fun".to_owned()) { + Ok(()) => false, + Err(e) => e.contains("Bad schema"), + }; assert_eq!( execute_fn(&(code_ok.to_owned()), &"test_fun".to_owned(), json!(10)), Ok("true".to_string()) ); assert_eq!(err_execute, true); - assert_eq!(compile_fn(&(code_ok.to_owned())), Ok(())); + assert_eq!( + compile_fn(&(code_ok.to_owned()), &"test_fun".to_owned()), + Ok(()) + ); assert_eq!(err_compile, true); } From f4dba3196804b50383cea85024bcd24996119ff7 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 20 Mar 2024 07:30:38 +0000 Subject: [PATCH 311/352] chore(version): v0.32.2 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea752b69..cd88a8bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.32.2 - 2024-03-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.23.2 +### Global changes +#### Bug Fixes +- PICAF-25598 auto-create variantIds dimension - (ee2e7dc) - ankit.mahato + +- - - + ## v0.32.1 - 2024-03-19 ### Package updates - context-aware-config bumped to context-aware-config-v0.23.1 diff --git a/Cargo.lock b/Cargo.lock index 5c81d9096..157ddac19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.23.1" +version = "0.23.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 96a67cad4..e6daaf524 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.23.2 - 2024-03-20 +#### Bug Fixes +- PICAF-25884 Functions bug fixes - (8e7452b) - ankit.mahato + +- - - + ## context-aware-config-v0.23.1 - 2024-03-19 #### Bug Fixes - [PICAF-26348] added routes without service prefix for b/w compatibility - (079c02d) - Shubhranshu Sanjeev diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 3c92193d3..359cd1133 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.23.1" +version = "0.23.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 3005cfce0b20f15c7aaa63de344e2226f214c1ae Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 8 Mar 2024 18:13:13 +0530 Subject: [PATCH 312/352] feat: refactor resolve page - Improve UI and UX - declutter the UI - Change context form to add dimension next to operator instead of above --- crates/frontend/src/app.rs | 2 +- .../components/context_form/context_form.rs | 32 +- .../src/components/dropdown/dropdown.rs | 2 +- .../components/override_form/override_form.rs | 2 +- ...ContextOverride.rs => context_override.rs} | 17 +- .../frontend/src/pages/ContextOverride/mod.rs | 2 +- crates/frontend/src/pages/Home/Home.rs | 523 ++++++++++-------- 7 files changed, 341 insertions(+), 239 deletions(-) rename crates/frontend/src/pages/ContextOverride/{ContextOverride.rs => context_override.rs} (96%) diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 8e489f52a..c10dae503 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -7,7 +7,7 @@ use crate::hoc::layout::layout::Layout; use crate::pages::experiment_list::experiment_list::ExperimentList; use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::{ - ContextOverride::ContextOverride::ContextOverride, + ContextOverride::context_override::ContextOverride, DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, Home::Home::Home, }; diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs index 5a9e162c8..d454fdb72 100644 --- a/crates/frontend/src/components/context_form/context_form.rs +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -12,6 +12,8 @@ pub fn context_form<NF>( context: Vec<(String, String, String)>, #[prop(default = String::new())] heading_sub_text: String, #[prop(default = false)] disabled: bool, + #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, + #[prop(default = false)] resolve_mode: bool, ) -> impl IntoView where NF: Fn(Vec<(String, String, String)>) + 'static, @@ -64,7 +66,7 @@ where dropdown_width="w-80" dropdown_icon="ri-add-line".to_string() dropdown_text="Add Context".to_string() - dropdown_direction=DropdownDirection::Left + dropdown_direction dropdown_options=dimensions.get_value() disabled=disabled on_select=Box::new(handle_select_dropdown_option) @@ -86,12 +88,24 @@ where let dimension_name = dimension.to_string(); view! { <div class="flex gap-x-6"> + <div class="form-control"> + <label class="label font-mono text-sm"> + <span class="label-text">Dimension</span> + </label> + <input + value=dimension_label + class="input w-full max-w-xs" + name="context-dimension-name" + disabled=true + /> + </div> <div class="form-control w-20"> <label class="label font-medium font-mono text-sm"> <span class="label-text">Operator</span> </label> + <select - disabled=disabled + disabled=disabled || resolve_mode value=operator.clone() on:input=move |event| { let input_value = event_target_value(&event); @@ -102,12 +116,15 @@ where } name="context-dimension-operator" - class="select select-bordered w-full text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" + class="select select-bordered w-full max-w-xs text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" > - <option disabled selected> + <option disabled selected=!resolve_mode> Pick one </option> - <option value="==" selected=operator.clone() == "=="> + <option + value="==" + selected=operator.clone() == "==" || resolve_mode + > "IS" </option> <option value="IN" selected=operator.clone() == "IN"> @@ -121,9 +138,7 @@ where </div> <div class="form-control"> <label class="label font-mono text-sm"> - <span class="label-text" name="context-dimension-name"> - {dimension_label} - </span> + <span class="label-text">Value</span> </label> <div class="flex gap-x-6 items-center"> <input @@ -195,6 +210,7 @@ where dropdown_text="Add Context".to_string() dropdown_options=dimensions disabled=disabled + dropdown_direction on_select=Box::new(handle_select_dropdown_option) /> } diff --git a/crates/frontend/src/components/dropdown/dropdown.rs b/crates/frontend/src/components/dropdown/dropdown.rs index 40faf7fdd..52d03253d 100644 --- a/crates/frontend/src/components/dropdown/dropdown.rs +++ b/crates/frontend/src/components/dropdown/dropdown.rs @@ -9,7 +9,7 @@ pub enum DropdownBtnType { Fill, } -#[derive(PartialEq)] +#[derive(PartialEq, Copy, Clone)] pub enum DropdownDirection { Right, Left, diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index 1464e1b27..b4a4a8b6e 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -41,7 +41,7 @@ where .filter(|config| !overrides.get().contains_key(&config.key)) .collect::<Vec<DefaultConfig>>() }); - let has_default_config = Signal::derive(move || unused_config_keys.get().len() > 0); + // let has_default_config = Signal::derive(move || unused_config_keys.get().len() > 0); let on_submit = move |event: MouseEvent| { event.prevent_default(); diff --git a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs b/crates/frontend/src/pages/ContextOverride/context_override.rs similarity index 96% rename from crates/frontend/src/pages/ContextOverride/ContextOverride.rs rename to crates/frontend/src/pages/ContextOverride/context_override.rs index 2abaa9f27..fd17fa020 100644 --- a/crates/frontend/src/pages/ContextOverride/ContextOverride.rs +++ b/crates/frontend/src/pages/ContextOverride/context_override.rs @@ -15,7 +15,7 @@ use serde_json::{Map, Value}; use web_sys::MouseEvent; #[component] -fn ContextModalForm<NF>( +fn context_modal_form<NF>( handle_change: NF, dimensions: Resource<String, Result<Vec<Dimension>, ServerFnError>>, ) -> impl IntoView @@ -68,7 +68,7 @@ where } #[component] -fn OverrideModalForm<NF>(handle_change: NF) -> impl IntoView +fn override_modal_form<NF>(handle_change: NF) -> impl IntoView where NF: Fn(Map<String, Value>) + 'static + Clone, { @@ -124,7 +124,7 @@ where } #[component] -fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { +fn modal_component(handle_submit: Rc<dyn Fn()>) -> impl IntoView { let (context_condition, set_context_condition) = create_signal::<Vec<(String, String, String)>>(vec![]); let (overrides, set_overrides) = create_signal::<Map<String, Value>>(Map::new()); @@ -155,12 +155,15 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { spawn_local({ let handle_submit = handle_submit_clone; + let overrides = move || overrides.get(); + let context_conditions = move || context_condition.get(); + let dimensions = move || dimensions.get(); async move { let result = create_context( current_tenant, - overrides.get(), - context_condition.get(), - dimensions.get().unwrap().expect("resource not loaded"), + overrides(), + context_conditions(), + dimensions().unwrap().expect("resource not loaded"), ) .await; @@ -217,7 +220,7 @@ fn ModalComponent(handle_submit: Rc<dyn Fn()>) -> impl IntoView { } #[component] -pub fn ContextOverride() -> impl IntoView { +pub fn context_override() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); let context_data: Vec<(String, String, String)> = vec![]; diff --git a/crates/frontend/src/pages/ContextOverride/mod.rs b/crates/frontend/src/pages/ContextOverride/mod.rs index 0f1d89ff1..a943f41f1 100644 --- a/crates/frontend/src/pages/ContextOverride/mod.rs +++ b/crates/frontend/src/pages/ContextOverride/mod.rs @@ -1 +1 @@ -pub mod ContextOverride; +pub mod context_override; diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs index 2ee25c02c..a7275339b 100644 --- a/crates/frontend/src/pages/Home/Home.rs +++ b/crates/frontend/src/pages/Home/Home.rs @@ -1,18 +1,33 @@ -use leptos::*; -use serde_json::{Map, Value}; -use wasm_bindgen::JsCast; -use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent}; +use std::time::Duration; +use crate::components::condition_pills::condition_pills::ContextPills; use crate::{ api::{fetch_config, fetch_dimensions}, components::{ - button::button::Button, - condition_pills::utils::{extract_and_format, parse_conditions}, - context_form::context_form::ContextForm, + button::button::Button, context_form::context_form::ContextForm, + dropdown::dropdown::DropdownDirection, }, - utils::{check_url_and_return_val, get_host}, + utils::{check_url_and_return_val, get_element_by_id, get_host}, +}; +use leptos::*; +use serde_json::{Map, Value}; +use strum::EnumProperty; +use strum_macros::Display; +use wasm_bindgen::JsCast; +use web_sys::{ + HtmlButtonElement, HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent, }; +#[derive(Clone, Debug, Copy, Display, strum_macros::EnumProperty, PartialEq)] +enum ResolveTab { + #[strum(props(id = "resolved_config_tab"))] + ResolvedConfig, + // #[strum(props(id = "selected_configs_tab"))] + // SelectedConfig, + #[strum(props(id = "all_configs_tab"))] + AllConfig, +} + async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { let client = reqwest::Client::new(); let host = get_host(); @@ -39,7 +54,6 @@ fn gen_name_id(s0: &String, s1: &String, s2: &String) -> String { #[component] pub fn home() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - // let (config_display_rs, config_display_ws) = create_signal(Map::new()); let config_data = create_blocking_resource( move || tenant_rs.get(), move |tenant| fetch_config(tenant), @@ -54,7 +68,7 @@ pub fn home() -> impl IntoView { }, ); - let (_display_configs_rs, _display_configs_ws) = create_signal(true); + let (selected_tab_rs, selected_tab_ws) = create_signal(ResolveTab::AllConfig); let unstrike = |search_field_prefix: &String, config: &Map<String, Value>| { for (dimension, value) in config.into_iter() { @@ -92,11 +106,15 @@ pub fn home() -> impl IntoView { let _ = config_name_element .class_list() .add_2("text-black", "font-bold"); - let _ = config_name_element.class_list().remove_1("text-gray-300"); + let _ = config_name_element + .class_list() + .remove_2("text-gray-300", "line-through"); let _ = config_value_element .class_list() .add_2("text-black", "font-bold"); - let _ = config_value_element.class_list().remove_1("text-gray-300"); + let _ = config_value_element + .class_list() + .remove_2("text-gray-300", "line-through"); logging::log!( "config name after replace {} and value {}", config_name_element.to_string(), @@ -129,9 +147,9 @@ pub fn home() -> impl IntoView { dimension_labels .item(i) .expect("missing input") - .dyn_ref::<HtmlSpanElement>() + .dyn_ref::<HtmlInputElement>() .unwrap() - .inner_text(), + .value(), dimension_ops .item(i) .expect("missing input") @@ -157,11 +175,15 @@ pub fn home() -> impl IntoView { let _ = config_name_element .class_list() .remove_2("text-black", "font-bold"); - let _ = config_name_element.class_list().add_1("text-gray-300"); + let _ = config_name_element + .class_list() + .add_2("text-gray-300", "line-through"); let _ = config_value_element .class_list() .remove_2("text-black", "font-bold"); - let _ = config_value_element.class_list().add_1("text-gray-300"); + let _ = config_value_element + .class_list() + .add_2("text-gray-300", "line-through"); } logging::log!("query vector {:#?}", query_vector); // resolve the context and get the config that would apply @@ -205,26 +227,28 @@ pub fn home() -> impl IntoView { logging::log!("unstrike default config if needed"); unstrike(&String::new(), &config); - let resolution_card = document() - .get_element_by_id("resolved_table_body") - .expect("resolve table card not found"); + if selected_tab_rs.get_untracked() == ResolveTab::ResolvedConfig { + let resolution_card = document() + .get_element_by_id("resolved_table_body") + .expect("resolve table card not found"); - let mut table_rows = String::new(); - for (key, value) in config.iter() { - table_rows.push_str( + let mut table_rows = String::new(); + for (key, value) in config.iter() { + table_rows.push_str( format!( "<tr><td>{key}</td><td style='word-break: break-word;'>{}</td></tr>", check_url_and_return_val(value.as_str().unwrap().to_owned()) ) .as_str(), ) + } + resolution_card.set_inner_html(&table_rows); } - resolution_card.set_inner_html(&table_rows); }); }; view! { - <div class="flex w-full flex-row mt-5 justify-evenly"> - <div class="card mr-5 ml-5 mt-6 h-4/5 shadow bg-base-100 w-4/12"> + <div class="flex w-full flex-col flex-wrap mt-5 justify-evenly"> + <div class="card mr-5 ml-5 mt-6 h-4/5 shadow bg-base-100"> <Suspense fallback=move || { view! { <p>"Loading..."</p> } }> @@ -232,18 +256,25 @@ pub fn home() -> impl IntoView { dimension_resource .with(|dimension| { view! { - <div class="card m-2 bg-base-100"> + <div class="card flex flex-row m-2 bg-base-100"> <div class="card-body"> <h2 class="card-title">Resolve Configs</h2> <ContextForm dimensions=dimension.to_owned().unwrap_or(vec![]) context=vec![] + heading_sub_text="Query your configs".to_string() + dropdown_direction=DropdownDirection::Right is_standalone=false + resolve_mode=true handle_change=|_| () /> - <div class="card-actions justify-end"> - <Button text="Resolve".to_string() on_click=resolve_click/> + <div class="card-actions mt-6 justify-end"> + <Button + id="resolve_btn".to_string() + text="Resolve".to_string() + on_click=resolve_click + /> </div> </div> </div> @@ -252,213 +283,265 @@ pub fn home() -> impl IntoView { }} </Suspense> - // config suspense - <Suspense fallback=move || { - view! { <p>"Loading..."</p> } - }> + </div> + <div role="tablist" class="tabs m-6 w-30 self-start tabs-lifted tabs-md"> + <a + role="tab" + id=ResolveTab::AllConfig.get_str("id").expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + ResolveTab::AllConfig => { + "tab tab-active [--tab-border-color:#a651f5] text-center" + } + _ => "tab", + } - {config_data - .with(move |conf| { - match conf { - Some(Ok(config)) => { - let default_configs = config.default_configs.clone(); - view! { - <div class="card m-2 bg-base-100"> - <div class="card-body"> - <h2 class="card-title">Resolved Config</h2> - <table class="table"> - <thead> - <tr> - <th>Config Key</th> - <th>Value</th> - </tr> - </thead> - <tbody id="resolved_table_body"> - <For - each=move || { default_configs.clone().into_iter() } + on:click=move |_| { + selected_tab_ws.set(ResolveTab::AllConfig); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > - key=|(key, value)| format!("{key}-{value}") - children=move |(config, value)| { - view! { - <tr> - <td>{config}</td> - <td style="word-break: break-word;"> - {match value { - Value::String(s) => check_url_and_return_val(s), - Value::Number(num) => num.to_string(), - Value::Bool(b) => b.to_string(), - _ => "".into(), - }} + All Contexts + </a> + <a + role="tab" + id=ResolveTab::ResolvedConfig + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + ResolveTab::ResolvedConfig => { + "tab tab-active [--tab-border-color:orange] text-center" + } + _ => "tab", + } - </td> + on:click=move |_| { + selected_tab_ws.set(ResolveTab::ResolvedConfig); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > - </tr> - } + Resolved Configuration + </a> + </div> + {move || { + selected_tab_rs + .with(|tab| { + match tab { + ResolveTab::AllConfig => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + {config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let rows = |k: &String, v: &Value, striked: bool| { + let mut view_vector = vec![]; + let default_iter = vec![(k.clone(), v.clone())]; + for (key, value) in v + .as_object() + .unwrap_or(&Map::from_iter(default_iter)) + .iter() + { + let key = key.replace("\"", "").trim().to_string(); + let value = value + .as_str() + .unwrap_or(&value.to_string().trim_matches('"')[..]) + .into(); + let unique_name = gen_name_id(k, &key, &value); + view_vector + .push( + view! { + < tr > < td class = "min-w-48 font-mono" > < span name = + format!("{unique_name}-1") class = "config-name" class : + text - black = { ! striked } class : font - bold = { ! + striked } class : text - gray - 300 = { striked } > { key } + </ span > </ td > < td class = "min-w-48 font-mono" style = + "word-break: break-word;" > < span name = + format!("{unique_name}-2") class = "config-value" class : + text - black = { ! striked } class : font - bold = { ! + striked } class : text - gray - 300 = { striked } > { + check_url_and_return_val(value) } </ span > </ td > </ tr > + }, + ) } - /> + view_vector + }; + let contexts_views: Vec<_> = config + .contexts + .iter() + .map(|context| { + let rows: Vec<_> = context + .override_with_keys + .iter() + .filter_map(|key| { + let o = config.overrides.get(key); + if o.is_some() { Some((key, o.unwrap())) } else { None } + }) + .map(|(k, v)| { rows(&k, &v, true) }) + .collect(); + view! { + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title"> + <ContextPills context=context.condition.clone()/> + </h2> + <table class="table table-zebra mt-10"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{rows}</tbody> + </table> - </tbody> - </table> - </div> - </div> - } - } - Some(Err(error)) => { - view! { - <div class="error"> - {"Failed to fetch config data: "} {error.to_string()} - </div> - } - } - None => { - view! { <div class="error">{"No config data fetched"}</div> } + </div> + </div> + } + }) + .collect::<Vec<_>>(); + let new_context_views = contexts_views + .into_iter() + .rev() + .collect::<Vec<_>>(); + let default_config: Vec<_> = config + .default_configs + .iter() + .map(|(k, v)| { rows(&k, &v, false) }) + .collect(); + vec![ + view! { + <div class="mb-4 overflow-y-scroll"> + {new_context_views} + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title">Default Configuration</h2> + <table class="table table-zebra"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{default_config}</tbody> + </table> + </div> + </div> + </div> + }, + ] + } + Some(Err(error)) => { + vec![ + view! { + <div class="error"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + }, + ] + } + None => { + vec![ + view! { + <div class="error">{"No config data fetched"}</div> + }, + ] + } + } + })} + + </Suspense> } } - })} + ResolveTab::ResolvedConfig => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> - </Suspense> - </div> - <Suspense fallback=move || { - view! { <p>"Loading (Suspense Fallback)..."</p> } - }> + {config_data + .with(move |conf| { + match conf { + Some(Ok(config)) => { + let default_configs = config.default_configs.clone(); + view! { + <div class="card m-6 shadow bg-base-100"> + <div class="card-body"> + <h2 class="card-title">Resolved Config</h2> + <table class="table table-zebra"> + <thead> + <tr> + <th>Config Key</th> + <th>Value</th> + </tr> + </thead> + <tbody id="resolved_table_body"> + <For + each=move || { default_configs.clone().into_iter() } - {config_data - .with(move |result| { - match result { - Some(Ok(config)) => { - let rows = |k: &String, v: &Value, striked: bool| { - let mut view_vector = vec![]; - println!("{:?}", v); - let default_iter = vec![(k.clone(), v.clone())]; - for (key, value) in v - .as_object() - .unwrap_or(&Map::from_iter(default_iter)) - .iter() - { - let key = key.replace("\"", "").trim().to_string(); - let value = format!("{}", value) - .replace("\"", "") - .trim() - .to_string(); - let unique_name = gen_name_id(k, &key, &value); - view_vector - .push( - view! { - < tr > < td > < span name = format!("{unique_name}-1") class - = "config-name" class : text - black = { ! striked } class : - font - bold = { ! striked } class : text - gray - 300 = { - striked } > { key } </ span > </ td > < td style="word-break: break-word;"> < span name = - format!("{unique_name}-2") class = "config-value" class : - text - black = { ! striked } class : font - bold = { ! - striked } class : text - gray - 300 = { striked } > { check_url_and_return_val(value) - } </ span > </ td > </ tr > - }, - ) - } - view_vector - }; - let contexts_views: Vec<_> = config - .contexts - .iter() - .map(|context| { - let condition = parse_conditions( - extract_and_format(&context.condition), - ); - let rows: Vec<_> = context - .override_with_keys - .iter() - .filter_map(|key| { - let o = config.overrides.get(key); - if o.is_some() { Some((key, o.unwrap())) } else { None } - }) - .map(|(k, v)| { rows(&k, &v, true) }) - .collect(); - view! { - <div class="card bg-base-100 shadow m-6"> - <div class="card-body"> - <h2 class="card-title"> - {condition - .iter() - .map(|(dim, op, val)| { - view! { - <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> - <span class="font-mono font-medium context_condition text-gray-500"> - {dim} - </span> - <span class="font-mono font-medium text-gray-650 context_condition "> - {op} - </span> - <span class="font-mono font-semibold context_condition"> - {val} - </span> - </span> - } - }) - .collect_view()} + key=|(key, value)| format!("{key}-{value}") + children=move |(config, value)| { + view! { + <tr> + <td>{config}</td> + <td style="word-break: break-word;"> + {match value { + Value::String(s) => check_url_and_return_val(s), + Value::Number(num) => num.to_string(), + Value::Bool(b) => b.to_string(), + _ => "".into(), + }} - </h2> - <table class="table mt-10"> - <thead> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody>{rows}</tbody> - </table> + </td> - </div> - </div> - } - }) - .collect::<Vec<_>>(); - let new_context_views = contexts_views - .into_iter() - .rev() - .collect::<Vec<_>>(); - let default_config: Vec<_> = config - .default_configs - .iter() - .map(|(k, v)| { rows(&k, &v, false) }) - .collect(); - vec![ - view! { - <div class="mb-4 w-8/12 overflow-y-auto max-h-screen"> - {new_context_views} - <div class="card bg-base-100 shadow m-6"> - <div class="card-body"> - <h2 class="card-title">Default Configuration</h2> - <table class="table"> - <thead> - <tr> - <th>Key</th> - <th>Value</th> - </tr> - </thead> - <tbody>{default_config}</tbody> - </table> - </div> - </div> - </div> - }, - ] - } - Some(Err(error)) => { - vec![ - view! { - <div class="error"> - {"Failed to fetch config data: "} {error.to_string()} - </div> - }, - ] - } - None => { - vec![view! { <div class="error">{"No config data fetched"}</div> }] + </tr> + } + } + /> + + </tbody> + </table> + </div> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="error"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + } + } + None => { + view! { + <div class="error">{"No config data fetched"}</div> + } + } + } + })} + + </Suspense> + } } } - })} + }) + }} - </Suspense> </div> } } From 693a0235023f1b482c346403b29723c080cb1c31 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 21 Mar 2024 06:42:09 +0000 Subject: [PATCH 313/352] chore(version): v0.33.0 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/frontend/CHANGELOG.md | 6 ++++++ crates/frontend/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd88a8bfc..610b07733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.33.0 - 2024-03-21 +### Package updates +- frontend bumped to frontend-v0.2.0 +### Global changes + +- - - + ## v0.32.2 - 2024-03-20 ### Package updates - context-aware-config bumped to context-aware-config-v0.23.2 diff --git a/Cargo.lock b/Cargo.lock index 157ddac19..ba55bf348 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,7 +1506,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.1.1" +version = "0.2.0" dependencies = [ "actix-files", "actix-web", diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index 7dd68d08e..6330d3aab 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.2.0 - 2024-03-21 +#### Features +- [PICAF-26197] refactor resolve page - (acc763a) - Kartik + +- - - + ## frontend-v0.1.1 - 2024-03-07 #### Bug Fixes - adding min-width settings for table component - (0fcd0c1) - Kartik diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 7fbc7c4cf..aaeaf4ca8 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.1.1" +version = "0.2.0" edition = "2021" [lib] From 0c61362a48736664c459338c2de7b99d9e697c09 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Mon, 11 Mar 2024 12:09:36 +0530 Subject: [PATCH 314/352] feat: Filter Config by prefix --- .../src/api/config/handlers.rs | 75 +++++++----- .../src/api/config/helpers.rs | 108 ++++++++++++++++++ .../src/api/config/mod.rs | 1 + 3 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 crates/context-aware-config/src/api/config/helpers.rs diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index c574245cc..211db5710 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -1,6 +1,11 @@ +use std::collections::HashSet; use std::{collections::HashMap, str::FromStr}; -use super::types::{Config, Context}; +use super::helpers::{ + filter_config_by_dimensions, filter_config_by_prefix, filter_context, +}; + +use super::types::Config; use crate::db::schema::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; @@ -17,10 +22,7 @@ use diesel::{ }; use serde_json::{json, Map, Value, Value::Null}; use service_utils::{ - errors::types::Error as err, - helpers::{extract_dimensions, ToActixErr}, - service::types::DbConnection, - types as app, + errors::types::Error as err, helpers::ToActixErr, service::types::DbConnection, }; pub fn endpoints() -> Scope { @@ -135,9 +137,43 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR if let Ok(true) = is_not_modified { return Ok(HttpResponse::NotModified().finish()); } - let res = HttpResponse::Ok().json(generate_cac(&mut conn).await?); - add_last_modified_header(max_created_at, res) + let params = Query::<HashMap<String, String>>::from_query(req.query_string()) + .map_err(|_| ErrorBadRequest("Unable to retrieve query parameters."))?; + let mut query_params_map: serde_json::Map<String, Value> = Map::new(); + + for (key, value) in params.0.into_iter() { + query_params_map.insert( + key, + value + .parse::<i32>() + .map_or_else(|_| json!(value), |int_val| json!(int_val)), + ); + } + + let config = generate_cac(&mut conn).await?; + + let filtered_config_by_dimensions = + filter_config_by_dimensions(&config, &query_params_map)?; + + let filtered_config_by_prefix = if let Some(prefix) = query_params_map.get("prefix") { + let prefix_list: HashSet<&str> = prefix + .as_str() + .ok_or_else(|| { + log::error!("Prefix is not a valid string."); + err::InternalServerErr("Prefix is not a valid string.".to_string()) + })? + .split(",") + .collect(); + filter_config_by_prefix(&filtered_config_by_dimensions, &prefix_list)? + } else { + filtered_config_by_dimensions + }; + + add_last_modified_header( + max_created_at, + HttpResponse::Ok().json(filtered_config_by_prefix), + ) } #[get("/resolve")] @@ -237,7 +273,7 @@ async fn get_filtered_config( let config = generate_cac(&mut conn).await?; let contexts = config.contexts; - let filtered_context = filter_context(contexts, query_params_map)?; + let filtered_context = filter_context(&contexts, &query_params_map)?; let mut filtered_overrides: Map<String, Value> = Map::new(); for ele in filtered_context.iter() { let override_with_key = &ele.override_with_keys[0]; @@ -261,26 +297,3 @@ async fn get_filtered_config( Ok(HttpResponse::Ok().json(filtered_config)) } - -fn filter_context( - contexts: Vec<Context>, - query_params_map: Map<String, Value>, -) -> app::Result<Vec<Context>> { - let mut filtered_context: Vec<Context> = Vec::new(); - for context in contexts.into_iter() { - if should_add_ctx(&context, &query_params_map)? { - filtered_context.push(context); - } - } - return Ok(filtered_context); -} - -fn should_add_ctx( - context: &Context, - query_params_map: &Map<String, Value>, -) -> app::Result<bool> { - let dimension = extract_dimensions(&context.condition)?; - Ok(dimension - .iter() - .all(|(key, value)| query_params_map.get(key).map_or(true, |val| val == value))) -} diff --git a/crates/context-aware-config/src/api/config/helpers.rs b/crates/context-aware-config/src/api/config/helpers.rs new file mode 100644 index 000000000..8b1d850bc --- /dev/null +++ b/crates/context-aware-config/src/api/config/helpers.rs @@ -0,0 +1,108 @@ +use std::collections::HashSet; + +use super::types::{Config, Context}; + +use serde_json::{Map, Value}; +use service_utils::{ + errors::types::Error as err, helpers::extract_dimensions, types as app, +}; + +pub fn filter_context( + contexts: &Vec<Context>, + query_params_map: &Map<String, Value>, +) -> app::Result<Vec<Context>> { + let mut filtered_context: Vec<Context> = Vec::new(); + for context in contexts.iter() { + if should_add_ctx(&context, query_params_map)? { + filtered_context.push(context.clone()); + } + } + return Ok(filtered_context); +} + +fn should_add_ctx( + context: &Context, + query_params_map: &Map<String, Value>, +) -> app::Result<bool> { + let dimension = extract_dimensions(&context.condition)?; + Ok(dimension + .iter() + .all(|(key, value)| query_params_map.get(key).map_or(true, |val| val == value))) +} + +pub fn filter_config_by_prefix( + config: &Config, + prefix_list: &HashSet<&str>, +) -> actix_web::Result<Config> { + let mut filtered_overrides: Map<String, Value> = Map::new(); + + let filtered_default_config: Map<String, Value> = config + .default_configs + .clone() + .into_iter() + .filter(|(key, _)| { + prefix_list + .iter() + .any(|prefix_str| key.starts_with(prefix_str)) + }) + .collect(); + + for (key, overrides) in &config.overrides { + let overrides_map = overrides + .as_object() + .ok_or_else(|| { + log::error!("failed to decode overrides."); + err::InternalServerErr("failed to decode overrides.".to_string()) + })? + .clone(); + + let filtered_overrides_map: Map<String, Value> = overrides_map + .into_iter() + .filter(|(key, _)| filtered_default_config.contains_key(key)) + .collect(); + + if !filtered_overrides_map.is_empty() { + filtered_overrides.insert(key.clone(), Value::Object(filtered_overrides_map)); + } + } + + let filtered_context: Vec<Context> = config + .contexts + .clone() + .into_iter() + .filter(|context| filtered_overrides.contains_key(&context.override_with_keys[0])) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: filtered_default_config, + }; + + Ok(filtered_config) +} + +pub fn filter_config_by_dimensions( + config: &Config, + query_params_map: &Map<String, Value>, +) -> actix_web::Result<Config> { + let filtered_context = filter_context(&config.contexts, &query_params_map)?; + let filtered_overrides: Map<String, Value> = filtered_context + .iter() + .flat_map(|ele| { + let override_with_key = &ele.override_with_keys[0]; + config + .overrides + .get(override_with_key) + .map(|value| (override_with_key.to_string(), value.clone())) + }) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs.clone(), + }; + + Ok(filtered_config) +} diff --git a/crates/context-aware-config/src/api/config/mod.rs b/crates/context-aware-config/src/api/config/mod.rs index ebe17b924..d4417bb8b 100644 --- a/crates/context-aware-config/src/api/config/mod.rs +++ b/crates/context-aware-config/src/api/config/mod.rs @@ -1,3 +1,4 @@ mod handlers; mod types; pub use handlers::endpoints; +mod helpers; From 7856b956723d38a76e5df1a19cc7bf8bc87c440c Mon Sep 17 00:00:00 2001 From: Saurav CV <saurav.cv@juspay.in> Date: Fri, 15 Mar 2024 12:30:14 +0530 Subject: [PATCH 315/352] fix: ui bug fix for contexts --- .../src/components/condition_pills/utils.rs | 80 ++++++++----------- .../src/components/experiment/experiment.rs | 35 ++++---- 2 files changed, 50 insertions(+), 65 deletions(-) diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs index 95b75d3b5..20300e8c6 100644 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -1,33 +1,21 @@ +use std::mem::swap; + use serde_json::Value; -pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { +pub fn parse_conditions( + input: Vec<Option<(String, String, String)>>, +) -> Vec<(String, String, String)> { let mut conditions = Vec::new(); - let operators = vec!["==", "in", "<="]; // Split the string by "&&" and iterate over each condition - for condition in input.split("&&") { - let mut parts = Vec::new(); - let mut operator_found = ""; - - // Check for each operator - for operator in &operators { - if condition.contains(operator) { - operator_found = operator; - parts = condition.split(operator).map(|s| s.trim()).collect(); - - // TODO: add this when context update is enabled - if parts.len() == 2 && operator == &"in" { - parts.swap(0, 1); - } - - break; + for opt_condition in input { + if let Some(condition) = opt_condition { + let mut key = condition.0; + let mut op = condition.1; + let mut val = condition.2; + if op == "in" { + swap(&mut key, &mut val) } - } - - if parts.len() == 2 { - let mut key = parts[0].to_string(); - let mut op = operator_found.to_string(); - let mut val = parts[1].to_string(); // Add a space after key key.push(' '); if op == "==".to_string() { @@ -49,7 +37,7 @@ pub fn parse_conditions(input: String) -> Vec<(String, String, String)> { conditions } -pub fn extract_and_format(condition: &Value) -> String { +pub fn extract_and_format(condition: &Value) -> Vec<Option<(String, String, String)>> { if condition.is_object() && condition.get("and").is_some() { // Handling complex "and" conditions let empty_vec = vec![]; @@ -63,14 +51,14 @@ pub fn extract_and_format(condition: &Value) -> String { formatted_conditions.push(format_condition(cond)); } - formatted_conditions.join(" && ") + formatted_conditions } else { // Handling single conditions - format_condition(condition) + vec![format_condition(condition)] } } -fn format_condition(condition: &Value) -> String { +fn format_condition(condition: &Value) -> Option<(String, String, String)> { if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { let empty_vec = vec![]; let operands = condition[operator].as_array().unwrap_or(&empty_vec); @@ -88,7 +76,7 @@ fn format_condition(condition: &Value) -> String { if right_operand.is_object() && right_operand["var"].is_string() { let var_str = right_operand["var"].as_str().unwrap(); - return format!("{} {} {}", left_str, operator, var_str); + return Some((left_str, operator.to_string(), var_str.to_string())); } } @@ -103,33 +91,31 @@ fn format_condition(condition: &Value) -> String { if mid_operand.is_object() && mid_operand["var"].is_string() { let var_str = mid_operand["var"].as_str().unwrap(); - return format!( - "{} {} {}", - var_str, - operator, - left_str + "," + &right_str - ); + return Some(( + var_str.to_string(), + operator.to_string(), + left_str + "," + &right_str, + )); } } // Handling regular operators if let Some(first_operand) = operands.get(0) { if first_operand.is_object() && first_operand["var"].is_string() { - let key = first_operand["var"].as_str().unwrap_or("UnknownVar"); + let key = first_operand["var"] + .as_str() + .unwrap_or("UnknownVar") + .to_string(); if let Some(value) = operands.get(1) { - if value.is_string() { - return format!( - "{} {} \"{}\"", - key, - operator, - value.as_str().unwrap() - ); - } else { - return format!("{} {} {}", key, operator, value); - } + let val_str = match value { + Value::Null => "null".to_string(), + Value::String(v) => v.clone(), + _ => value.to_string(), + }; + return Some((key.to_string(), operator.to_string(), val_str)); } } } } - "Invalid Condition".to_string() + None } diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index bae58b504..d37ca4883 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use leptos::*; -use crate::components::condition_pills::utils::extract_and_format; +use crate::components::condition_pills::utils::{extract_and_format, parse_conditions}; use crate::components::table::table::Table; use super::utils::gen_variant_table; @@ -156,25 +156,24 @@ where <h2 class="card-title">Context</h2> <div class="flex flex-row flex-wrap gap-2"> {move || { - let context = contexts.clone(); let mut view = Vec::new(); - let tokens = context.split("&&"); - for token in tokens.into_iter() { - let mut t = token.trim().split(" "); - let (dimension, _, value) = (t.next(), t.next(), t.next()); - view.push( - view! { - <div class="stat w-3/12"> - <div class="stat-title"> - {format!("{}", dimension.unwrap())} + for token in contexts.clone() { + if let Some(t) = token{ + let (dimension, value) = (t.0, t.2); + view.push( + view! { + <div class="stat w-3/12"> + <div class="stat-title"> + {dimension} + </div> + <div class="stat-value text-base"> + {&value.replace("\"", "")} + + </div> </div> - <div class="stat-value text-base"> - {format!("{}", &value.unwrap().replace("\"", ""))} - - </div> - </div> - }, - ); + }, + ); + } } view }} From fb14036abd88e0de90b3f45d311ccc406324c06b Mon Sep 17 00:00:00 2001 From: Saurav CV <saurav.cv@juspay.in> Date: Wed, 20 Mar 2024 15:45:48 +0530 Subject: [PATCH 316/352] fix: added type for condition --- .../condition_pills/condition_pills.rs | 7 +- .../src/components/condition_pills/mod.rs | 1 + .../src/components/condition_pills/types.rs | 6 ++ .../src/components/condition_pills/utils.rs | 82 +++++++++++-------- .../src/components/experiment/experiment.rs | 4 +- 5 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 crates/frontend/src/components/condition_pills/types.rs diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs index 0f5391f4b..5c180e6e4 100644 --- a/crates/frontend/src/components/condition_pills/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -10,12 +10,15 @@ pub fn context_pills(context: Value) -> impl IntoView { view! { {ctx_values .into_iter() - .map(|(dim, op, val)| { + .map(|condition| { + let dimension = condition.left_operand; + let op = condition.operator; + let val = condition.right_operand; let operator = op.clone(); view! { <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> <span class="font-mono font-medium context_condition text-gray-500"> - {dim} + {dimension} </span> <span class="font-mono font-medium text-gray-650 context_condition "> {op} diff --git a/crates/frontend/src/components/condition_pills/mod.rs b/crates/frontend/src/components/condition_pills/mod.rs index bad1e7b1e..2343919bf 100644 --- a/crates/frontend/src/components/condition_pills/mod.rs +++ b/crates/frontend/src/components/condition_pills/mod.rs @@ -1,2 +1,3 @@ pub mod condition_pills; +pub mod types; pub mod utils; diff --git a/crates/frontend/src/components/condition_pills/types.rs b/crates/frontend/src/components/condition_pills/types.rs new file mode 100644 index 000000000..05681bb18 --- /dev/null +++ b/crates/frontend/src/components/condition_pills/types.rs @@ -0,0 +1,6 @@ +#[derive(Clone)] +pub struct Condition { + pub left_operand: String, + pub operator: String, + pub right_operand: String, +} diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs index 20300e8c6..bd6f93ffa 100644 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -1,43 +1,49 @@ use std::mem::swap; +use super::types::Condition; use serde_json::Value; -pub fn parse_conditions( - input: Vec<Option<(String, String, String)>>, -) -> Vec<(String, String, String)> { +pub fn parse_conditions(input: Vec<Condition>) -> Vec<Condition> { let mut conditions = Vec::new(); // Split the string by "&&" and iterate over each condition - for opt_condition in input { - if let Some(condition) = opt_condition { - let mut key = condition.0; - let mut op = condition.1; - let mut val = condition.2; - if op == "in" { - swap(&mut key, &mut val) - } - // Add a space after key - key.push(' '); - if op == "==".to_string() { + for condition in input { + let mut key = condition.left_operand; + let mut op = condition.operator; + let mut val = condition.right_operand; + if op == "in" { + swap(&mut key, &mut val) + } + // Add a space after key + key.push(' '); + match op.as_str() { + "==" => { val = val.trim_matches('"').to_string(); op = "is".to_string(); - } else if op == "<=".to_string() { + } + "<=" => { val = val.trim_matches('"').to_string(); op = "BETWEEN".to_string(); - } else { + } + _ => { val = val.trim_matches('"').to_string(); op = "has".to_string(); } - op.push(' '); - - conditions.push((key, op, val)); } + op.push(' '); + + conditions.push(Condition { + left_operand: key, + operator: op, + right_operand: val, + }); } conditions } -pub fn extract_and_format(condition: &Value) -> Vec<Option<(String, String, String)>> { +pub fn extract_and_format(condition: &Value) -> Vec<Condition> { + let mut formatted_conditions = Vec::new(); if condition.is_object() && condition.get("and").is_some() { // Handling complex "and" conditions let empty_vec = vec![]; @@ -46,19 +52,19 @@ pub fn extract_and_format(condition: &Value) -> Vec<Option<(String, String, Stri .and_then(|val| val.as_array()) .unwrap_or(&empty_vec); // Default to an empty vector if not an array - let mut formatted_conditions = Vec::new(); for cond in conditions_json { - formatted_conditions.push(format_condition(cond)); + if let Some(formatted_condition) = format_condition(cond) { + formatted_conditions.push(formatted_condition); + } } - - formatted_conditions - } else { + } else if let Some(formatted_condition) = format_condition(condition) { // Handling single conditions - vec![format_condition(condition)] + formatted_conditions.push(formatted_condition); } + formatted_conditions } -fn format_condition(condition: &Value) -> Option<(String, String, String)> { +fn format_condition(condition: &Value) -> Option<Condition> { if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { let empty_vec = vec![]; let operands = condition[operator].as_array().unwrap_or(&empty_vec); @@ -76,7 +82,11 @@ fn format_condition(condition: &Value) -> Option<(String, String, String)> { if right_operand.is_object() && right_operand["var"].is_string() { let var_str = right_operand["var"].as_str().unwrap(); - return Some((left_str, operator.to_string(), var_str.to_string())); + return Some(Condition { + left_operand: left_str, + operator: operator.to_string(), + right_operand: var_str.to_string(), + }); } } @@ -91,11 +101,11 @@ fn format_condition(condition: &Value) -> Option<(String, String, String)> { if mid_operand.is_object() && mid_operand["var"].is_string() { let var_str = mid_operand["var"].as_str().unwrap(); - return Some(( - var_str.to_string(), - operator.to_string(), - left_str + "," + &right_str, - )); + return Some(Condition { + left_operand: var_str.to_string(), + operator: operator.to_string(), + right_operand: left_str + "," + &right_str, + }); } } // Handling regular operators @@ -111,7 +121,11 @@ fn format_condition(condition: &Value) -> Option<(String, String, String)> { Value::String(v) => v.clone(), _ => value.to_string(), }; - return Some((key.to_string(), operator.to_string(), val_str)); + return Some(Condition { + left_operand: key.to_string(), + operator: operator.to_string(), + right_operand: val_str, + }); } } } diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index d37ca4883..34f90c61e 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -158,8 +158,7 @@ where {move || { let mut view = Vec::new(); for token in contexts.clone() { - if let Some(t) = token{ - let (dimension, value) = (t.0, t.2); + let (dimension, value) = (token.left_operand, token.right_operand); view.push( view! { <div class="stat w-3/12"> @@ -173,7 +172,6 @@ where </div> }, ); - } } view }} From bd807ec1a7684d5d0fbd4d75acfc024cede9fee5 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 21 Mar 2024 11:06:56 +0000 Subject: [PATCH 317/352] chore(version): v0.34.0 [skip ci] --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 4 ++-- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/frontend/CHANGELOG.md | 7 +++++++ crates/frontend/Cargo.toml | 2 +- 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 610b07733..e55115a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.34.0 - 2024-03-21 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.0 +- frontend bumped to frontend-v0.2.1 +### Global changes + +- - - + ## v0.33.0 - 2024-03-21 ### Package updates - frontend bumped to frontend-v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index ba55bf348..97f9203f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.23.2" +version = "0.24.0" dependencies = [ "actix", "actix-cors", @@ -1506,7 +1506,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.2.0" +version = "0.2.1" dependencies = [ "actix-files", "actix-web", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index e6daaf524..ff756c6fd 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.24.0 - 2024-03-21 +#### Features +- PICAF-26307 Filter Config by prefix - (c0a0bfe) - ankit.mahato + +- - - + ## context-aware-config-v0.23.2 - 2024-03-20 #### Bug Fixes - PICAF-25884 Functions bug fixes - (8e7452b) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 359cd1133..d4a94e80c 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.23.2" +version = "0.24.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index 6330d3aab..7c7175148 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.2.1 - 2024-03-21 +#### Bug Fixes +- PICAF-26324 added type for condition - (3607336) - Saurav CV +- PICAF-26324 ui bug fix for contexts - (7ec15ec) - Saurav CV + +- - - + ## frontend-v0.2.0 - 2024-03-21 #### Features - [PICAF-26197] refactor resolve page - (acc763a) - Kartik diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index aaeaf4ca8..83abd2873 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.2.0" +version = "0.2.1" edition = "2021" [lib] From 6201a2da7dae26bdd0f3d72be3a0c9bdf5cc7e58 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Thu, 21 Mar 2024 17:19:50 +0530 Subject: [PATCH 318/352] fix: filter config fix --- .../src/api/config/handlers.rs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 211db5710..cda13ac90 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -151,12 +151,8 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR ); } - let config = generate_cac(&mut conn).await?; - - let filtered_config_by_dimensions = - filter_config_by_dimensions(&config, &query_params_map)?; - - let filtered_config_by_prefix = if let Some(prefix) = query_params_map.get("prefix") { + let mut config = generate_cac(&mut conn).await?; + if let Some(prefix) = query_params_map.get("prefix") { let prefix_list: HashSet<&str> = prefix .as_str() .ok_or_else(|| { @@ -165,15 +161,16 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR })? .split(",") .collect(); - filter_config_by_prefix(&filtered_config_by_dimensions, &prefix_list)? - } else { - filtered_config_by_dimensions - }; + config = filter_config_by_prefix(&config, &prefix_list)? + } + + query_params_map.remove("prefix"); + + if !query_params_map.is_empty() { + config = filter_config_by_dimensions(&config, &query_params_map)? + } - add_last_modified_header( - max_created_at, - HttpResponse::Ok().json(filtered_config_by_prefix), - ) + add_last_modified_header(max_created_at, HttpResponse::Ok().json(config)) } #[get("/resolve")] From 9a458c62bdd491579360f877898343e892dee4fe Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Thu, 21 Mar 2024 12:59:00 +0000 Subject: [PATCH 319/352] chore(version): v0.34.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e55115a53..dbd63f05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.34.1 - 2024-03-21 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.1 +### Global changes + +- - - + ## v0.34.0 - 2024-03-21 ### Package updates - context-aware-config bumped to context-aware-config-v0.24.0 diff --git a/Cargo.lock b/Cargo.lock index 97f9203f3..d0b45ba87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.24.0" +version = "0.24.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index ff756c6fd..cc1be022d 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.24.1 - 2024-03-21 +#### Bug Fixes +- PICAF-26307 filter config fix - (aa114fb) - ankit.mahato + +- - - + ## context-aware-config-v0.24.0 - 2024-03-21 #### Features - PICAF-26307 Filter Config by prefix - (c0a0bfe) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index d4a94e80c..b87d97f7b 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.24.0" +version = "0.24.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From aad2b7a0e2c056cbd4f1c21bdac094e1ee01be9a Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Wed, 27 Mar 2024 12:13:59 +0530 Subject: [PATCH 320/352] fix: JS validator functions to take config value and key --- .../src/api/context/helpers.rs | 4 ++-- .../src/api/default_config/handlers.rs | 9 ++++++--- .../src/api/functions/handlers.rs | 10 +++++----- .../src/api/functions/types.rs | 7 +++++++ .../src/validation_functions.rs | 14 +++++++++++--- crates/context-aware-config/tests/cac_tests.rs | 8 +++++++- 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context-aware-config/src/api/context/helpers.rs index 57de21203..844581297 100644 --- a/crates/context-aware-config/src/api/context/helpers.rs +++ b/crates/context-aware-config/src/api/context/helpers.rs @@ -114,9 +114,9 @@ fn validate_value_with_function( key: &String, value: &Value, ) -> anyhow::Result<()> { - let base64_decoded = BASE64_STANDARD.decode(function.clone())?; + let base64_decoded = BASE64_STANDARD.decode(function)?; let utf8_decoded = str::from_utf8(&base64_decoded)?; - if let Err((e, stdout)) = execute_fn(&utf8_decoded, fun_name, value.to_owned()) { + if let Err((e, stdout)) = execute_fn(&utf8_decoded, fun_name, key, value.to_owned()) { log::error!("function validation failed for {key} with error: {e}"); return Err(anyhow!(json!({ "message": format!("function validation failed for {} with error {}", key, e), "stdout": stdout diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 8c8438cbb..78a14ca49 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -142,9 +142,12 @@ async fn create( ErrorInternalServerError(json!({"message": e.to_string()})) })?; - if let Err((e, stdout)) = - execute_fn(&utf8_decoded, &f_name, default_config.value.to_owned()) - { + if let Err((e, stdout)) = execute_fn( + &utf8_decoded, + &f_name, + &key, + default_config.value.to_owned(), + ) { log::info!("function validation failed for {key} with error: {e}"); return Err(ErrorBadRequest(json!({ "message": format!( "function validation failed with error: {}", e), "stdout": stdout diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index d71f3a389..1142826de 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -4,7 +4,7 @@ use base64::prelude::*; use super::helpers::{decode_function, fetch_function}; use crate::{ - api::functions::types::{Stage, TestParam}, + api::functions::types::{Stage, TestFunctionRequest, TestParam}, db::{ self, models::Function, @@ -22,7 +22,7 @@ use actix_web::{ use chrono::Utc; use dashboard_auth::types::User; use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; -use serde_json::{json, Value}; +use serde_json::json; use service_utils::service::types::DbConnection; use validation_functions::{compile_fn, execute_fn}; @@ -238,7 +238,7 @@ async fn delete_function( #[put("/{function_name}/{stage}/test")] async fn test( params: Path<TestParam>, - request: web::Json<Value>, + request: web::Json<TestFunctionRequest>, db_conn: DbConnection, ) -> actix_web::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; @@ -260,9 +260,9 @@ async fn test( }; decode_function(&mut function)?; let result = match path_params.stage { - Stage::DRAFT => execute_fn(&function.draft_code, fun_name, req), + Stage::DRAFT => execute_fn(&function.draft_code, fun_name, &req.key, req.value), Stage::PUBLISHED => match function.published_code { - Some(code) => execute_fn(&code, fun_name, req), + Some(code) => execute_fn(&code, fun_name, &req.key, req.value), None => { log::error!("Function test failed: function not published yet"); Err(( diff --git a/crates/context-aware-config/src/api/functions/types.rs b/crates/context-aware-config/src/api/functions/types.rs index 246effd0c..39a39b40c 100644 --- a/crates/context-aware-config/src/api/functions/types.rs +++ b/crates/context-aware-config/src/api/functions/types.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_json::Value; #[derive(Debug, Deserialize)] pub struct UpdateFunctionRequest { @@ -38,3 +39,9 @@ pub struct TestParam { pub function_name: String, pub stage: Stage, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct TestFunctionRequest { + pub key: String, + pub value: Value, +} diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index dd5993ab0..6380e53a3 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -52,19 +52,27 @@ const ES_LINT_CODE: &str = r#" "#; -fn runtime_wrapper(function_name: &str, value: Value) -> String { - let fun_call: String = format!("const fun_value = {}({});", function_name, value); +fn runtime_wrapper(function_name: &str, key: &str, value: Value) -> String { + let fun_call: String = format!( + "const fun_value = {}({});", + function_name, + json!({ + "key": key, + "value": value + }) + ); fun_call + EXIT_LOGIC_CODE } pub fn execute_fn( code_str: &str, fun_name: &str, + key: &str, value: Value, ) -> Result<String, (String, Option<String>)> { let output = Command::new("node") .arg("-e") - .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, value)) + .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, key, value)) .output(); log::trace!("{}", format!("validation function output : {:?}", output)); match output { diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index f41f30b92..35f7395c2 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -24,6 +24,7 @@ fn test_execute_fn() { let err_execute = match execute_fn( &(execute_code_error.to_owned()), &"test_fun".to_owned(), + "test", json!(10), ) { Ok(_) => false, @@ -35,7 +36,12 @@ fn test_execute_fn() { Err(e) => e.contains("Bad schema"), }; assert_eq!( - execute_fn(&(code_ok.to_owned()), &"test_fun".to_owned(), json!(10)), + execute_fn( + &(code_ok.to_owned()), + &"test_fun".to_owned(), + "test", + json!(10) + ), Ok("true".to_string()) ); assert_eq!(err_execute, true); From 7b3c3e7c9692d0b1ef2680c3aea20b759593894d Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 27 Mar 2024 14:23:31 +0000 Subject: [PATCH 321/352] chore(version): v0.34.2 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd63f05b..1ce238391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.34.2 - 2024-03-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.2 +### Global changes + +- - - + ## v0.34.1 - 2024-03-21 ### Package updates - context-aware-config bumped to context-aware-config-v0.24.1 diff --git a/Cargo.lock b/Cargo.lock index d0b45ba87..470177b39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.24.1" +version = "0.24.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index cc1be022d..85af6e8a4 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.24.2 - 2024-03-27 +#### Bug Fixes +- PICAF-26454 JS validator functions to take config value and key - (656fe39) - ankit.mahato + +- - - + ## context-aware-config-v0.24.1 - 2024-03-21 #### Bug Fixes - PICAF-26307 filter config fix - (aa114fb) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index b87d97f7b..7f4fed41d 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.24.1" +version = "0.24.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 85751e00e9ec0d4e3719e373a79b3886c7b06f9b Mon Sep 17 00:00:00 2001 From: Natarajan Kannan <natarajan@juspay.in> Date: Thu, 8 Feb 2024 19:25:04 +0530 Subject: [PATCH 322/352] docs: add intro doc and features feat: added HS cac client fix: a time bug with last_modified feat: added experimentation client feat: a working nix build feat: migrate from naersk to crane feat: flake parts added feat: nixified hs-cac-client feat: completed nixification --- clients/hs-cac-client/CHANGELOG.md | 11 ++ clients/hs-cac-client/LICENSE | 30 ++++ clients/hs-cac-client/README.md | 1 + clients/hs-cac-client/Setup.hs | 2 + clients/hs-cac-client/default.nix | 37 +++++ clients/hs-cac-client/hs-cac-client.cabal | 118 +++++++++++++++ clients/hs-cac-client/src/Client.hs | 121 +++++++++++++++ clients/hs-cac-client/src/Main.hs | 26 ++++ clients/hs-exp-client/CHANGELOG.md | 11 ++ clients/hs-exp-client/LICENSE | 30 ++++ clients/hs-exp-client/README.md | 1 + clients/hs-exp-client/Setup.hs | 2 + clients/hs-exp-client/default.nix | 37 +++++ clients/hs-exp-client/hs-exp-client.cabal | 119 +++++++++++++++ clients/hs-exp-client/src/Client.hs | 128 ++++++++++++++++ clients/hs-exp-client/src/Main.hs | 34 +++++ crates/cac_client/build.rs | 4 +- crates/cac_client/default.nix | 63 ++++++++ crates/cac_client/rust-toolchain.toml | 2 + .../experimentation-platform/src/db/schema.rs | 2 +- crates/superposition_client/default.nix | 60 ++++++++ .../superposition_client/rust-toolchain.toml | 2 + crates/superposition_client/src/lib.rs | 1 + flake.lock | 64 ++++---- flake.nix | 141 ++++++++++++++---- headers/libsuperposition_client.h | 20 +++ libs | 1 + 27 files changed, 1002 insertions(+), 66 deletions(-) create mode 100644 clients/hs-cac-client/CHANGELOG.md create mode 100644 clients/hs-cac-client/LICENSE create mode 100644 clients/hs-cac-client/README.md create mode 100644 clients/hs-cac-client/Setup.hs create mode 100644 clients/hs-cac-client/default.nix create mode 100644 clients/hs-cac-client/hs-cac-client.cabal create mode 100644 clients/hs-cac-client/src/Client.hs create mode 100644 clients/hs-cac-client/src/Main.hs create mode 100644 clients/hs-exp-client/CHANGELOG.md create mode 100644 clients/hs-exp-client/LICENSE create mode 100644 clients/hs-exp-client/README.md create mode 100644 clients/hs-exp-client/Setup.hs create mode 100644 clients/hs-exp-client/default.nix create mode 100644 clients/hs-exp-client/hs-exp-client.cabal create mode 100644 clients/hs-exp-client/src/Client.hs create mode 100644 clients/hs-exp-client/src/Main.hs create mode 100644 crates/cac_client/default.nix create mode 100644 crates/cac_client/rust-toolchain.toml create mode 100644 crates/superposition_client/default.nix create mode 100644 crates/superposition_client/rust-toolchain.toml create mode 120000 libs diff --git a/clients/hs-cac-client/CHANGELOG.md b/clients/hs-cac-client/CHANGELOG.md new file mode 100644 index 000000000..9d0666ba0 --- /dev/null +++ b/clients/hs-cac-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-cac-client/LICENSE b/clients/hs-cac-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/hs-cac-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-cac-client/README.md b/clients/hs-cac-client/README.md new file mode 100644 index 000000000..1c2d58957 --- /dev/null +++ b/clients/hs-cac-client/README.md @@ -0,0 +1 @@ +# hs-cac-client diff --git a/clients/hs-cac-client/Setup.hs b/clients/hs-cac-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/hs-cac-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/hs-cac-client/default.nix b/clients/hs-cac-client/default.nix new file mode 100644 index 000000000..b67cf868f --- /dev/null +++ b/clients/hs-cac-client/default.nix @@ -0,0 +1,37 @@ +{ + perSystem = { config, pkgs, lib, self', ... }: { + haskellProjects.cac = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + cac_client.custom = _: self'.packages.clients; + }; + }; + packages.hs-cac-client = self'.packages.cac-hs-cac-client.overrideAttrs (oa: { + cac_client = self'.packages.clients; + nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ + pkgs.makeWrapper + ]; + # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 + postFixup = (oa.postFixup or "") + '' + wrapProgram $out/bin/* \ + --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $cac_client/lib + ''; + }); + apps.cac.program = lib.getExe self'.packages.cac; + + devShells.cac-client = pkgs.mkShell { + name = "hs-cac-client"; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + config.flake-root.devShell + ]; + # TODO: set once, based on platform + # TODO(refactor): SRID: can we do this in one place? + # LD_LIBRARY_PATH = "${self'.packages.cac_client}/lib"; + shellHook = '' + export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" + ''; + }; + }; +} diff --git a/clients/hs-cac-client/hs-cac-client.cabal b/clients/hs-cac-client/hs-cac-client.cabal new file mode 100644 index 000000000..3d70ba9a0 --- /dev/null +++ b/clients/hs-cac-client/hs-cac-client.cabal @@ -0,0 +1,118 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-cac-client +version: 0.1.0.0 +description: Please see the README on GitHub at <https://github.com/githubuser/hs#readme> +homepage: https://github.com/githubuser/hs#readme +bug-reports: https://github.com/githubuser/hs/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs + +library + exposed-modules: + Client + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exe + main-is: Main.hs + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + cac_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-cac-client + default-language: Haskell2010 diff --git a/clients/hs-cac-client/src/Client.hs b/clients/hs-cac-client/src/Client.hs new file mode 100644 index 000000000..eb0aaefaa --- /dev/null +++ b/clients/hs-cac-client/src/Client.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( createCacClient +, getCacClient +, getCacConfig +, getCacLastModified +, cacEval +, cacStartPolling +) where + +import Data.Aeson +import Data.Functor (($>)) +import Foreign.C.String (CString, newCAString, peekCAString) +import Foreign.C.Types (CInt (CInt), CULong (..)) +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Ptr +import Prelude + +data Arc_Client + +type CacClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_cac_client :: CTenant -> Bool -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_cac_client :: FunPtr (Ptr CacClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_cac_client :: CTenant -> IO (Ptr CacClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "get_last_modified" + c_get_last_modified_time :: Ptr CacClient -> IO CString + +foreign import ccall unsafe "get_config" + c_get_config :: Ptr CacClient -> IO CString + +foreign import ccall unsafe "cac_eval" + c_cac_eval :: Ptr CacClient -> CString -> IO CString + +foreign import ccall safe "start_polling_update" + c_cac_poll :: CTenant -> IO () + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +cacStartPolling :: Tenant -> IO () +cacStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_cac_poll + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createCacClient:: Tenant -> Bool -> Integer -> String -> IO (Either Error ()) +createCacClient tenant shouldUpdate frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_cac_client cTenant shouldUpdate duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) +getCacClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_cac_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_cac_client cacClient + +getCacConfig :: ForeignPtr CacClient -> IO (Either Error Value) +getCacConfig client = do + config <- withForeignPtr client c_get_config + if config == nullPtr + then Left <$> getError + else do + fptrConfig <- newForeignPtr c_free_string config + Right . toJSON <$> withForeignPtr fptrConfig peekCAString + +getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) +getCacLastModified client = do + lastModified <- withForeignPtr client c_get_last_modified_time + if lastModified == nullPtr + then Left <$> getError + else do + fptrLastModified <- newForeignPtr c_free_string lastModified + Right <$> withForeignPtr fptrLastModified peekCAString + +cacEval :: ForeignPtr CacClient -> String -> IO (Either Error Value) +cacEval client context = do + cContext <- newCAString context + overrides <- withForeignPtr client (`c_cac_eval` cContext) + _ <- cleanup [cContext] + if overrides == nullPtr + then Left <$> getError + else do + fptrOverrides <- newForeignPtr c_free_string overrides + Right . toJSON <$> withForeignPtr fptrOverrides peekCAString diff --git a/clients/hs-cac-client/src/Main.hs b/clients/hs-cac-client/src/Main.hs new file mode 100644 index 000000000..d3530ab1d --- /dev/null +++ b/clients/hs-cac-client/src/Main.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (cacEval, createCacClient, getCacClient, + getCacConfig, getCacLastModified, cacStartPolling) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createCacClient "dev" True 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkOS (cacStartPolling "dev") + print threadId + getCacClient "dev" >>= \case + Left err -> putStrLn err + Right client -> do + config <- getCacConfig client + lastModified <- getCacLastModified client + overrides <- cacEval client "{\"os\": \"android\", \"client\": \"2mg\"}" + print config + print lastModified + print overrides + threadDelay 1000000000 + pure () diff --git a/clients/hs-exp-client/CHANGELOG.md b/clients/hs-exp-client/CHANGELOG.md new file mode 100644 index 000000000..e43174087 --- /dev/null +++ b/clients/hs-exp-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs-exp-client` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-exp-client/LICENSE b/clients/hs-exp-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/hs-exp-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-exp-client/README.md b/clients/hs-exp-client/README.md new file mode 100644 index 000000000..8401db649 --- /dev/null +++ b/clients/hs-exp-client/README.md @@ -0,0 +1 @@ +# hs-exp-client diff --git a/clients/hs-exp-client/Setup.hs b/clients/hs-exp-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/hs-exp-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/hs-exp-client/default.nix b/clients/hs-exp-client/default.nix new file mode 100644 index 000000000..1c6136789 --- /dev/null +++ b/clients/hs-exp-client/default.nix @@ -0,0 +1,37 @@ +{ + perSystem = { config, pkgs, lib, self', ... }: { + haskellProjects.exp = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + superposition_client.custom = _: self'.packages.clients; + }; + }; + packages.hs-exp-client = self'.packages.exp-hs-exp-client.overrideAttrs (oa: { + superposition_client = self'.packages.clients; + nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ + pkgs.makeWrapper + ]; + # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 + postFixup = (oa.postFixup or "") + '' + wrapProgram $out/bin/* \ + --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $superposition_client/lib + ''; + }); + apps.cac.program = lib.getExe self'.packages.cac; + + devShells.exp-client = pkgs.mkShell { + name = "hs-exp-client"; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + config.flake-root.devShell + ]; + # TODO: set once, based on platform + # TODO(refactor): SRID: can we do this in one place? + # LD_LIBRARY_PATH = "${self'.packages.superposition_client}/lib"; + shellHook = '' + export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" + ''; + }; + }; +} diff --git a/clients/hs-exp-client/hs-exp-client.cabal b/clients/hs-exp-client/hs-exp-client.cabal new file mode 100644 index 000000000..cfa266e54 --- /dev/null +++ b/clients/hs-exp-client/hs-exp-client.cabal @@ -0,0 +1,119 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-exp-client +version: 0.1.0.0 +description: Please see the README on GitHub at <https://github.com/githubuser/hs-exp-client#readme> +homepage: https://github.com/githubuser/hs-exp-client#readme +bug-reports: https://github.com/githubuser/hs-exp-client/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs-exp-client + +library + exposed-modules: + + Client + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exp-client-exe + main-is: Main.hs + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + superposition_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-exp-client + default-language: Haskell2010 diff --git a/clients/hs-exp-client/src/Client.hs b/clients/hs-exp-client/src/Client.hs new file mode 100644 index 000000000..7e28353e9 --- /dev/null +++ b/clients/hs-exp-client/src/Client.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( expStartPolling +, getExpClient +, createExpClient +, getApplicableVariants +, getSatisfiedExperiments +, getRunningExperiments +) where + +import Data.Aeson.Types +import Data.Functor (($>)) +import Foreign (FunPtr, Ptr) +import Foreign.C (CInt (..), CShort (..), CULong (..)) +import Foreign.C.String +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Ptr (nullPtr) +import Prelude + +data Arc_Client + +type ExpClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_exp_client :: CTenant -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_exp_client :: FunPtr (Ptr ExpClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_exp_client :: CTenant -> IO (Ptr ExpClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +foreign import ccall unsafe "start_polling_update" + c_start_polling_update :: CTenant -> IO () + +foreign import ccall unsafe "get_applicable_variant" + c_get_applicable_variants :: Ptr ExpClient -> CString -> CShort -> IO CString + +foreign import ccall unsafe "get_satisfied_experiments" + c_get_satisfied_experiments :: Ptr ExpClient -> CString -> IO CString + +foreign import ccall unsafe "get_running_experiments" + c_get_running_experiments :: Ptr ExpClient -> IO CString + +expStartPolling :: Tenant -> IO () +expStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_start_polling_update + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) +createExpClient tenant frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_exp_client cTenant duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) +getExpClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_exp_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_exp_client cacClient + +getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +getApplicableVariants client query toss = do + context <- newCAString query + variants <- withForeignPtr client (\c -> c_get_applicable_variants c context (fromInteger toss)) + _ <- cleanup [context] + if variants == nullPtr + then Left <$> getError + else do + fptrVariants <- newForeignPtr c_free_string variants + Right <$> withForeignPtr fptrVariants peekCAString + -- pure $ + -- case fromJSON variantVector of + -- Error s -> Left s + -- Success vec -> Right vec + +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +getSatisfiedExperiments client query = do + context <- newCAString query + experiments <- withForeignPtr client (`c_get_satisfied_experiments` context) + _ <- cleanup [context] + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString + +getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) +getRunningExperiments client = do + experiments <- withForeignPtr client c_get_running_experiments + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString diff --git a/clients/hs-exp-client/src/Main.hs b/clients/hs-exp-client/src/Main.hs new file mode 100644 index 000000000..bb7621266 --- /dev/null +++ b/clients/hs-exp-client/src/Main.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (createExpClient, expStartPolling, + getApplicableVariants, getExpClient, + getRunningExperiments, + getSatisfiedExperiments) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createExpClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkIO (expStartPolling "dev") + print threadId + getExpClient "dev" >>= \case + Left err -> putStrLn err + Right client -> loop client + pure () + where + loop client = do + runningExperiments <- getRunningExperiments client + satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" + variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + print "Running experiments" + print runningExperiments + print "experiments that satisfy context" + print satisfiedExperiments + print "variant ID applied" + print variants + -- threadDelay 10000000 + loop client diff --git a/crates/cac_client/build.rs b/crates/cac_client/build.rs index cb9d3c4d2..5f6fd1d0b 100644 --- a/crates/cac_client/build.rs +++ b/crates/cac_client/build.rs @@ -6,6 +6,6 @@ fn main() { config.language = cbindgen::Language::C; println!("Calling build.rs in cac_client"); cbindgen::generate_with_config(&crate_dir, config) - .unwrap() - .write_to_file("../../headers/libcac_client.h"); + .unwrap() + .write_to_file("../../headers/libcac_client.h"); } diff --git a/crates/cac_client/default.nix b/crates/cac_client/default.nix new file mode 100644 index 000000000..b0c2af779 --- /dev/null +++ b/crates/cac_client/default.nix @@ -0,0 +1,63 @@ +# Nix for Rust project management +{ inputs, ... }: { + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + args = { + pname = "cac_client"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cargoVendorDir = craneLib.vendorCargoDeps { + src = ./../../.; + }; + cargoArtifacts = craneLib.buildDepsOnly args; + package = craneLib.buildPackage (args // { + inherit cargoArtifacts; + doCheck = false; # FIXME: tests require services to be running + }); + + check = craneLib.cargoClippy (args // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + packages.cac_client = package; + + checks.clippy = check; + + # # Flake outputs + # devShells.rust-exp = pkgs.mkShell { + # inputsFrom = [ + # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + # ]; + # shellHook = '' + # # For rust-analyzer 'hover' tooltips to work. + # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + # ''; + # nativeBuildInputs = with pkgs; [ + # # Add your dev tools here. + # rustToolchain + # cargo-watch + # ]; + # }; + }; +} diff --git a/crates/cac_client/rust-toolchain.toml b/crates/cac_client/rust-toolchain.toml new file mode 100644 index 000000000..31578d3bf --- /dev/null +++ b/crates/cac_client/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index a8676f480..6cd391ae0 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -616,4 +616,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m11, event_log_y2026m12, experiments, -); +); \ No newline at end of file diff --git a/crates/superposition_client/default.nix b/crates/superposition_client/default.nix new file mode 100644 index 000000000..a78ee5e5d --- /dev/null +++ b/crates/superposition_client/default.nix @@ -0,0 +1,60 @@ +# Nix for Rust project management +{ inputs, ... }: { + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + args = { + pname = "superposition_client"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cargoArtifacts = craneLib.buildDepsOnly args; + package = craneLib.buildPackage (args // { + inherit cargoArtifacts; + doCheck = false; # FIXME: tests require services to be running + }); + + check = craneLib.cargoClippy (args // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + packages.superposition_client = package; + + checks.clippy = check; + + # # Flake outputs + # devShells.rust-exp = pkgs.mkShell { + # inputsFrom = [ + # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + # ]; + # shellHook = '' + # # For rust-analyzer 'hover' tooltips to work. + # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + # ''; + # nativeBuildInputs = with pkgs; [ + # # Add your dev tools here. + # rustToolchain + # cargo-watch + # ]; + # }; + }; +} diff --git a/crates/superposition_client/rust-toolchain.toml b/crates/superposition_client/rust-toolchain.toml new file mode 100644 index 000000000..31578d3bf --- /dev/null +++ b/crates/superposition_client/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index a71143021..df009f7bb 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -1,5 +1,6 @@ mod interface; mod types; +mod interface; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; diff --git a/flake.lock b/flake.lock index 02944e91f..4dc7a1e83 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1707461758, - "narHash": "sha256-VaqINICYEtVKF0X+chdNtXcNp6poZr385v6AG7j0ybM=", + "lastModified": 1705974079, + "narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=", "owner": "ipetkov", "repo": "crane", - "rev": "505976eaeac289fe41d074bee37006ac094636bb", + "rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2", "type": "github" }, "original": { @@ -25,11 +25,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1706830856, - "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", + "lastModified": 1704982712, + "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", + "rev": "07f6395285469419cf9d078f59b5b49993198c00", "type": "github" }, "original": { @@ -58,11 +58,11 @@ }, "haskell-flake": { "locked": { - "lastModified": 1707242163, - "narHash": "sha256-w+cBynh7yqnpVtFdu1SEZxPgtlz/nWnv47D5crnPXHM=", + "lastModified": 1705984248, + "narHash": "sha256-1zW2udPF4clKCuTgk0FHjGUB7x/Tfe8m9oUhe3hM/YQ=", "owner": "srid", "repo": "haskell-flake", - "rev": "f9d17c3aa68e65529f424816c8b9346ae602d1de", + "rev": "cea3332b027264e914673df392870ebb1e92de44", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1707268954, - "narHash": "sha256-2en1kvde3cJVc3ZnTy8QeD2oKcseLFjYPLKhIGDanQ0=", + "lastModified": 1706550542, + "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1", + "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", "type": "github" }, "original": { @@ -88,6 +88,24 @@ } }, "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1703961334, + "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "dir": "lib", "lastModified": 1706550542, @@ -111,16 +129,13 @@ "flake-parts": "flake-parts", "haskell-flake": "haskell-flake", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay", - "systems": "systems_2" + "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1707444620, @@ -150,21 +165,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index fa16e1000..33fa4d909 100644 --- a/flake.nix +++ b/flake.nix @@ -1,45 +1,124 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + crane.url = "github:ipetkov/crane"; + crane.inputs.nixpkgs.follows = "nixpkgs"; haskell-flake.url = "github:srid/haskell-flake"; - systems.url = "github:nix-systems/default"; - crane = { - url = "github:ipetkov/crane"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + rust-overlay = { url = "github:oxalica/rust-overlay"; }; }; - outputs = inputs: + outputs = inputs@{ self, rust-overlay, flake-parts, crane, nixpkgs, haskell-flake, ... }: - inputs.flake-parts.lib.mkFlake { inherit inputs; } { - systems = import inputs.systems; + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ]; imports = [ inputs.haskell-flake.flakeModule - ./clients/haskell - ./rust.nix + ./clients/hs-cac-client + ./clients/hs-exp-client ]; + perSystem = { config, self', inputs', system, pkgs, lib, haskell-flake, ... }: + let + craneLib = crane.lib.${system}; + cac_args = { + pname = "cac_client"; + src = ./crates/cac_client; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + exp_args = { + pname = "superposition_client"; + src = ./crates/superposition_client; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]) ++ [ + pkgs.libiconv + pkgs.openssl + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + cacCargoArtifacts = craneLib.buildDepsOnly cac_args; + expCargoArtifacts = craneLib.buildDepsOnly exp_args; + cac_client = craneLib.buildPackage (cac_args // { + inherit cacCargoArtifacts; + }); + superposition_client = craneLib.buildPackage (exp_args // { + inherit expCargoArtifacts; + }); + in + rec { + _module.args.pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlay ]; + + }; + formatter = pkgs.nixpkgs-fmt; + # For `nix build` & `nix run`: + + packages.clients = craneLib.buildPackage { + nativeBuildInputs = with pkgs; [ postgresql_12 iconv darwin.apple_sdk.frameworks.Security ]; + src = craneLib.path ./.; # FIXME: move rust stuff to subdir + }; + + # For `nix develop`: + devShells.default = pkgs.mkShell { + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + # bring your local shell properties into nix env + # shellHook = '' + # echo "you are now in the nix shell" + # eval $($SHELL) + # ''; + nativeBuildInputs = + let - perSystem = { pkgs, self', ... }: { - devShells.default = pkgs.mkShell { - inputsFrom = [ - self'.devShells.rust - ]; - packages = with pkgs; [ - docker-compose - # Why do we need this? - stdenv.cc - awscli - jq - nodejs_18 - nixpkgs-fmt - ]; + univPkgs = with pkgs; [ + # Build requirements + cargo + libiconv + openssl + postgresql_12 + # Extras + rust-analyzer + rustfmt + bacon + cargo-watch + clippy + diesel-cli + docker-compose + stdenv.cc + pkg-config + awscli + jq + pkgs.nodejs_18 + leptosfmt + wasm-pack + curl + (rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + targets = [ "wasm32-unknown-unknown" ]; + }) + ]; + darwinPkgs = with pkgs; [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + ]; + in + univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else [ ]); + }; }; - }; }; -} \ No newline at end of file +} diff --git a/headers/libsuperposition_client.h b/headers/libsuperposition_client.h index f37cbf580..a3c1a629d 100644 --- a/headers/libsuperposition_client.h +++ b/headers/libsuperposition_client.h @@ -24,3 +24,23 @@ char *get_applicable_variant(struct Arc_Client *client, const char *c_context, s char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); char *get_running_experiments(struct Arc_Client *client); + +int last_error_length(void); + +const char *last_error_message(void); + +void free_string(char *s); + +int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); + +void start_polling_update(const char *tenant); + +void free_client(struct Arc_Client *ptr); + +struct Arc_Client *get_client(const char *tenant); + +char *get_applicable_variant(struct Arc_Client *client, const char *c_context, short toss); + +char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); + +char *get_running_experiments(struct Arc_Client *client); diff --git a/libs b/libs new file mode 120000 index 000000000..6e0cd6fa6 --- /dev/null +++ b/libs @@ -0,0 +1 @@ +/nix/store/wap6n7b93hm0vnwn4kx8yafxb4yl7lwc-hs-cac-client-0.1.0.0 \ No newline at end of file From af6ea75df2682e556606c8fe03d1d4e2d872f0e0 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 2 Feb 2024 19:28:32 +0530 Subject: [PATCH 323/352] feat: haskell client for superposition Co-authored-by: Shivaraj B H <shivaraj-bh@users.noreply.github.com> --- clients/hs-cac-client/CHANGELOG.md | 11 -- clients/hs-cac-client/LICENSE | 30 ---- clients/hs-cac-client/README.md | 1 - clients/hs-cac-client/Setup.hs | 2 - clients/hs-cac-client/default.nix | 37 ----- clients/hs-cac-client/hs-cac-client.cabal | 118 --------------- clients/hs-cac-client/src/Client.hs | 121 --------------- clients/hs-cac-client/src/Main.hs | 26 ---- clients/hs-exp-client/CHANGELOG.md | 11 -- clients/hs-exp-client/LICENSE | 30 ---- clients/hs-exp-client/README.md | 1 - clients/hs-exp-client/Setup.hs | 2 - clients/hs-exp-client/default.nix | 37 ----- clients/hs-exp-client/hs-exp-client.cabal | 119 --------------- clients/hs-exp-client/src/Client.hs | 128 ---------------- clients/hs-exp-client/src/Main.hs | 34 ----- crates/cac_client/default.nix | 63 -------- crates/cac_client/rust-toolchain.toml | 2 - .../experimentation-platform/src/db/schema.rs | 2 +- crates/superposition_client/default.nix | 60 -------- .../superposition_client/rust-toolchain.toml | 2 - crates/superposition_client/src/lib.rs | 1 + flake.lock | 64 ++++---- flake.nix | 141 ++++-------------- libs | 1 - 25 files changed, 65 insertions(+), 979 deletions(-) delete mode 100644 clients/hs-cac-client/CHANGELOG.md delete mode 100644 clients/hs-cac-client/LICENSE delete mode 100644 clients/hs-cac-client/README.md delete mode 100644 clients/hs-cac-client/Setup.hs delete mode 100644 clients/hs-cac-client/default.nix delete mode 100644 clients/hs-cac-client/hs-cac-client.cabal delete mode 100644 clients/hs-cac-client/src/Client.hs delete mode 100644 clients/hs-cac-client/src/Main.hs delete mode 100644 clients/hs-exp-client/CHANGELOG.md delete mode 100644 clients/hs-exp-client/LICENSE delete mode 100644 clients/hs-exp-client/README.md delete mode 100644 clients/hs-exp-client/Setup.hs delete mode 100644 clients/hs-exp-client/default.nix delete mode 100644 clients/hs-exp-client/hs-exp-client.cabal delete mode 100644 clients/hs-exp-client/src/Client.hs delete mode 100644 clients/hs-exp-client/src/Main.hs delete mode 100644 crates/cac_client/default.nix delete mode 100644 crates/cac_client/rust-toolchain.toml delete mode 100644 crates/superposition_client/default.nix delete mode 100644 crates/superposition_client/rust-toolchain.toml delete mode 120000 libs diff --git a/clients/hs-cac-client/CHANGELOG.md b/clients/hs-cac-client/CHANGELOG.md deleted file mode 100644 index 9d0666ba0..000000000 --- a/clients/hs-cac-client/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog for `hs` - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to the -[Haskell Package Versioning Policy](https://pvp.haskell.org/). - -## Unreleased - -## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-cac-client/LICENSE b/clients/hs-cac-client/LICENSE deleted file mode 100644 index 98e22917d..000000000 --- a/clients/hs-cac-client/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright Author name here (c) 2024 - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Author name here nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-cac-client/README.md b/clients/hs-cac-client/README.md deleted file mode 100644 index 1c2d58957..000000000 --- a/clients/hs-cac-client/README.md +++ /dev/null @@ -1 +0,0 @@ -# hs-cac-client diff --git a/clients/hs-cac-client/Setup.hs b/clients/hs-cac-client/Setup.hs deleted file mode 100644 index 9a994af67..000000000 --- a/clients/hs-cac-client/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/clients/hs-cac-client/default.nix b/clients/hs-cac-client/default.nix deleted file mode 100644 index b67cf868f..000000000 --- a/clients/hs-cac-client/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - perSystem = { config, pkgs, lib, self', ... }: { - haskellProjects.cac = { - projectRoot = ./.; - autoWire = [ "packages" "checks" "apps" ]; - settings = { - cac_client.custom = _: self'.packages.clients; - }; - }; - packages.hs-cac-client = self'.packages.cac-hs-cac-client.overrideAttrs (oa: { - cac_client = self'.packages.clients; - nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ - pkgs.makeWrapper - ]; - # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 - postFixup = (oa.postFixup or "") + '' - wrapProgram $out/bin/* \ - --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $cac_client/lib - ''; - }); - apps.cac.program = lib.getExe self'.packages.cac; - - devShells.cac-client = pkgs.mkShell { - name = "hs-cac-client"; - inputsFrom = [ - config.haskellProjects.default.outputs.devShell - config.flake-root.devShell - ]; - # TODO: set once, based on platform - # TODO(refactor): SRID: can we do this in one place? - # LD_LIBRARY_PATH = "${self'.packages.cac_client}/lib"; - shellHook = '' - export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" - ''; - }; - }; -} diff --git a/clients/hs-cac-client/hs-cac-client.cabal b/clients/hs-cac-client/hs-cac-client.cabal deleted file mode 100644 index 3d70ba9a0..000000000 --- a/clients/hs-cac-client/hs-cac-client.cabal +++ /dev/null @@ -1,118 +0,0 @@ -cabal-version: 2.2 - --- This file has been generated from package.yaml by hpack version 0.36.0. --- --- see: https://github.com/sol/hpack - -name: hs-cac-client -version: 0.1.0.0 -description: Please see the README on GitHub at <https://github.com/githubuser/hs#readme> -homepage: https://github.com/githubuser/hs#readme -bug-reports: https://github.com/githubuser/hs/issues -author: Author name here -maintainer: example@example.com -copyright: 2024 Author name here -license: BSD-3-Clause -license-file: LICENSE -build-type: Simple -extra-source-files: - README.md - CHANGELOG.md - -source-repository head - type: git - location: https://github.com/githubuser/hs - -library - exposed-modules: - Client - other-modules: - Paths_hs_cac_client - autogen-modules: - Paths_hs_cac_client - hs-source-dirs: - src - default-extensions: - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DuplicateRecordFields - ExplicitNamespaces - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedStrings - PatternSynonyms - PolyKinds - RankNTypes - RecordWildCards - ScopedTypeVariables - TupleSections - TypeApplications - TypeFamilies - TypeOperators - ViewPatterns - BlockArguments - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded - build-depends: - aeson - , base >=4.7 && <5 - , basic-prelude - default-language: Haskell2010 - -executable hs-exe - main-is: Main.hs - other-modules: - Paths_hs_cac_client - autogen-modules: - Paths_hs_cac_client - hs-source-dirs: - src - default-extensions: - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DuplicateRecordFields - ExplicitNamespaces - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedStrings - PatternSynonyms - PolyKinds - RankNTypes - RecordWildCards - ScopedTypeVariables - TupleSections - TypeApplications - TypeFamilies - TypeOperators - ViewPatterns - BlockArguments - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N - extra-libraries: - cac_client - build-depends: - aeson - , base >=4.7 && <5 - , basic-prelude - , hs-cac-client - default-language: Haskell2010 diff --git a/clients/hs-cac-client/src/Client.hs b/clients/hs-cac-client/src/Client.hs deleted file mode 100644 index eb0aaefaa..000000000 --- a/clients/hs-cac-client/src/Client.hs +++ /dev/null @@ -1,121 +0,0 @@ -{-# LANGUAGE ForeignFunctionInterface #-} -{-# OPTIONS_GHC -Wno-incomplete-patterns #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -{-# HLINT ignore "Use camelCase" #-} - -module Client -( createCacClient -, getCacClient -, getCacConfig -, getCacLastModified -, cacEval -, cacStartPolling -) where - -import Data.Aeson -import Data.Functor (($>)) -import Foreign.C.String (CString, newCAString, peekCAString) -import Foreign.C.Types (CInt (CInt), CULong (..)) -import Foreign.ForeignPtr -import Foreign.Marshal.Alloc (free) -import Foreign.Ptr -import Prelude - -data Arc_Client - -type CacClient = Arc_Client - -type CTenant = CString -type Tenant = String - -type Error = String - -foreign import ccall unsafe "new_client" - c_new_cac_client :: CTenant -> Bool -> CULong -> CString -> IO CInt - -foreign import ccall unsafe "&free_client" - c_free_cac_client :: FunPtr (Ptr CacClient -> IO ()) - -foreign import ccall unsafe "get_client" - c_get_cac_client :: CTenant -> IO (Ptr CacClient) - -foreign import ccall unsafe "last_error_message" - c_last_error_message :: IO CString - -foreign import ccall unsafe "get_last_modified" - c_get_last_modified_time :: Ptr CacClient -> IO CString - -foreign import ccall unsafe "get_config" - c_get_config :: Ptr CacClient -> IO CString - -foreign import ccall unsafe "cac_eval" - c_cac_eval :: Ptr CacClient -> CString -> IO CString - -foreign import ccall safe "start_polling_update" - c_cac_poll :: CTenant -> IO () - -foreign import ccall unsafe "&free_string" - c_free_string :: FunPtr (CString -> IO ()) - -cacStartPolling :: Tenant -> IO () -cacStartPolling tenant = - newCAString tenant - >>= newForeignPtr c_free_string - >>= flip withForeignPtr c_cac_poll - -getError :: IO String -getError = c_last_error_message - >>= newForeignPtr c_free_string - >>= flip withForeignPtr peekCAString - -cleanup :: [Ptr a] -> IO () -cleanup items = mapM free items $> () - -createCacClient:: Tenant -> Bool -> Integer -> String -> IO (Either Error ()) -createCacClient tenant shouldUpdate frequency hostname = do - let duration = fromInteger frequency - cTenant <- newCAString tenant - cHostname <- newCAString hostname - resp <- c_new_cac_client cTenant shouldUpdate duration cHostname - _ <- cleanup [cTenant, cHostname] - case resp of - 0 -> pure $ Right () - _ -> Left <$> getError - -getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) -getCacClient tenant = do - cTenant <- newCAString tenant - cacClient <- c_get_cac_client cTenant - _ <- cleanup [cTenant] - if cacClient == nullPtr - then Left <$> getError - else Right <$> newForeignPtr c_free_cac_client cacClient - -getCacConfig :: ForeignPtr CacClient -> IO (Either Error Value) -getCacConfig client = do - config <- withForeignPtr client c_get_config - if config == nullPtr - then Left <$> getError - else do - fptrConfig <- newForeignPtr c_free_string config - Right . toJSON <$> withForeignPtr fptrConfig peekCAString - -getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) -getCacLastModified client = do - lastModified <- withForeignPtr client c_get_last_modified_time - if lastModified == nullPtr - then Left <$> getError - else do - fptrLastModified <- newForeignPtr c_free_string lastModified - Right <$> withForeignPtr fptrLastModified peekCAString - -cacEval :: ForeignPtr CacClient -> String -> IO (Either Error Value) -cacEval client context = do - cContext <- newCAString context - overrides <- withForeignPtr client (`c_cac_eval` cContext) - _ <- cleanup [cContext] - if overrides == nullPtr - then Left <$> getError - else do - fptrOverrides <- newForeignPtr c_free_string overrides - Right . toJSON <$> withForeignPtr fptrOverrides peekCAString diff --git a/clients/hs-cac-client/src/Main.hs b/clients/hs-cac-client/src/Main.hs deleted file mode 100644 index d3530ab1d..000000000 --- a/clients/hs-cac-client/src/Main.hs +++ /dev/null @@ -1,26 +0,0 @@ -{-# LANGUAGE LambdaCase #-} -module Main (main) where - -import Client (cacEval, createCacClient, getCacClient, - getCacConfig, getCacLastModified, cacStartPolling) -import Control.Concurrent -import Prelude - -main :: IO () -main = do - createCacClient "dev" True 10 "http://localhost:8080" >>= \case - Left err -> putStrLn err - Right _ -> pure () - threadId <- forkOS (cacStartPolling "dev") - print threadId - getCacClient "dev" >>= \case - Left err -> putStrLn err - Right client -> do - config <- getCacConfig client - lastModified <- getCacLastModified client - overrides <- cacEval client "{\"os\": \"android\", \"client\": \"2mg\"}" - print config - print lastModified - print overrides - threadDelay 1000000000 - pure () diff --git a/clients/hs-exp-client/CHANGELOG.md b/clients/hs-exp-client/CHANGELOG.md deleted file mode 100644 index e43174087..000000000 --- a/clients/hs-exp-client/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog for `hs-exp-client` - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to the -[Haskell Package Versioning Policy](https://pvp.haskell.org/). - -## Unreleased - -## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/hs-exp-client/LICENSE b/clients/hs-exp-client/LICENSE deleted file mode 100644 index 98e22917d..000000000 --- a/clients/hs-exp-client/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright Author name here (c) 2024 - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Author name here nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/hs-exp-client/README.md b/clients/hs-exp-client/README.md deleted file mode 100644 index 8401db649..000000000 --- a/clients/hs-exp-client/README.md +++ /dev/null @@ -1 +0,0 @@ -# hs-exp-client diff --git a/clients/hs-exp-client/Setup.hs b/clients/hs-exp-client/Setup.hs deleted file mode 100644 index 9a994af67..000000000 --- a/clients/hs-exp-client/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/clients/hs-exp-client/default.nix b/clients/hs-exp-client/default.nix deleted file mode 100644 index 1c6136789..000000000 --- a/clients/hs-exp-client/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - perSystem = { config, pkgs, lib, self', ... }: { - haskellProjects.exp = { - projectRoot = ./.; - autoWire = [ "packages" "checks" "apps" ]; - settings = { - superposition_client.custom = _: self'.packages.clients; - }; - }; - packages.hs-exp-client = self'.packages.exp-hs-exp-client.overrideAttrs (oa: { - superposition_client = self'.packages.clients; - nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ - pkgs.makeWrapper - ]; - # https://gist.github.com/CMCDragonkai/9b65cbb1989913555c203f4fa9c23374 - postFixup = (oa.postFixup or "") + '' - wrapProgram $out/bin/* \ - --set ${if pkgs.stdenv.isLinux then "LD_LIBRARY_PATH" else "DYLD_LIBRARY_PATH"} $superposition_client/lib - ''; - }); - apps.cac.program = lib.getExe self'.packages.cac; - - devShells.exp-client = pkgs.mkShell { - name = "hs-exp-client"; - inputsFrom = [ - config.haskellProjects.default.outputs.devShell - config.flake-root.devShell - ]; - # TODO: set once, based on platform - # TODO(refactor): SRID: can we do this in one place? - # LD_LIBRARY_PATH = "${self'.packages.superposition_client}/lib"; - shellHook = '' - export DYLD_LIBRARY_PATH="${self'.packages.clients}/lib" - ''; - }; - }; -} diff --git a/clients/hs-exp-client/hs-exp-client.cabal b/clients/hs-exp-client/hs-exp-client.cabal deleted file mode 100644 index cfa266e54..000000000 --- a/clients/hs-exp-client/hs-exp-client.cabal +++ /dev/null @@ -1,119 +0,0 @@ -cabal-version: 2.2 - --- This file has been generated from package.yaml by hpack version 0.36.0. --- --- see: https://github.com/sol/hpack - -name: hs-exp-client -version: 0.1.0.0 -description: Please see the README on GitHub at <https://github.com/githubuser/hs-exp-client#readme> -homepage: https://github.com/githubuser/hs-exp-client#readme -bug-reports: https://github.com/githubuser/hs-exp-client/issues -author: Author name here -maintainer: example@example.com -copyright: 2024 Author name here -license: BSD-3-Clause -license-file: LICENSE -build-type: Simple -extra-source-files: - README.md - CHANGELOG.md - -source-repository head - type: git - location: https://github.com/githubuser/hs-exp-client - -library - exposed-modules: - - Client - other-modules: - Paths_hs_exp_client - autogen-modules: - Paths_hs_exp_client - hs-source-dirs: - src - default-extensions: - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DuplicateRecordFields - ExplicitNamespaces - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedStrings - PatternSynonyms - PolyKinds - RankNTypes - RecordWildCards - ScopedTypeVariables - TupleSections - TypeApplications - TypeFamilies - TypeOperators - ViewPatterns - BlockArguments - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded - build-depends: - aeson - , base >=4.7 && <5 - , basic-prelude - default-language: Haskell2010 - -executable hs-exp-client-exe - main-is: Main.hs - other-modules: - Paths_hs_exp_client - autogen-modules: - Paths_hs_exp_client - hs-source-dirs: - src - default-extensions: - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DuplicateRecordFields - ExplicitNamespaces - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedStrings - PatternSynonyms - PolyKinds - RankNTypes - RecordWildCards - ScopedTypeVariables - TupleSections - TypeApplications - TypeFamilies - TypeOperators - ViewPatterns - BlockArguments - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N - extra-libraries: - superposition_client - build-depends: - aeson - , base >=4.7 && <5 - , basic-prelude - , hs-exp-client - default-language: Haskell2010 diff --git a/clients/hs-exp-client/src/Client.hs b/clients/hs-exp-client/src/Client.hs deleted file mode 100644 index 7e28353e9..000000000 --- a/clients/hs-exp-client/src/Client.hs +++ /dev/null @@ -1,128 +0,0 @@ -{-# LANGUAGE ForeignFunctionInterface #-} -{-# OPTIONS_GHC -Wno-incomplete-patterns #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -{-# HLINT ignore "Use camelCase" #-} - -module Client -( expStartPolling -, getExpClient -, createExpClient -, getApplicableVariants -, getSatisfiedExperiments -, getRunningExperiments -) where - -import Data.Aeson.Types -import Data.Functor (($>)) -import Foreign (FunPtr, Ptr) -import Foreign.C (CInt (..), CShort (..), CULong (..)) -import Foreign.C.String -import Foreign.ForeignPtr -import Foreign.Marshal.Alloc (free) -import Foreign.Ptr (nullPtr) -import Prelude - -data Arc_Client - -type ExpClient = Arc_Client - -type CTenant = CString -type Tenant = String - -type Error = String - -foreign import ccall unsafe "new_client" - c_new_exp_client :: CTenant -> CULong -> CString -> IO CInt - -foreign import ccall unsafe "&free_client" - c_free_exp_client :: FunPtr (Ptr ExpClient -> IO ()) - -foreign import ccall unsafe "get_client" - c_get_exp_client :: CTenant -> IO (Ptr ExpClient) - -foreign import ccall unsafe "last_error_message" - c_last_error_message :: IO CString - -foreign import ccall unsafe "&free_string" - c_free_string :: FunPtr (CString -> IO ()) - -foreign import ccall unsafe "start_polling_update" - c_start_polling_update :: CTenant -> IO () - -foreign import ccall unsafe "get_applicable_variant" - c_get_applicable_variants :: Ptr ExpClient -> CString -> CShort -> IO CString - -foreign import ccall unsafe "get_satisfied_experiments" - c_get_satisfied_experiments :: Ptr ExpClient -> CString -> IO CString - -foreign import ccall unsafe "get_running_experiments" - c_get_running_experiments :: Ptr ExpClient -> IO CString - -expStartPolling :: Tenant -> IO () -expStartPolling tenant = - newCAString tenant - >>= newForeignPtr c_free_string - >>= flip withForeignPtr c_start_polling_update - -getError :: IO String -getError = c_last_error_message - >>= newForeignPtr c_free_string - >>= flip withForeignPtr peekCAString - -cleanup :: [Ptr a] -> IO () -cleanup items = mapM free items $> () - -createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) -createExpClient tenant frequency hostname = do - let duration = fromInteger frequency - cTenant <- newCAString tenant - cHostname <- newCAString hostname - resp <- c_new_exp_client cTenant duration cHostname - _ <- cleanup [cTenant, cHostname] - case resp of - 0 -> pure $ Right () - _ -> Left <$> getError - -getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) -getExpClient tenant = do - cTenant <- newCAString tenant - cacClient <- c_get_exp_client cTenant - _ <- cleanup [cTenant] - if cacClient == nullPtr - then Left <$> getError - else Right <$> newForeignPtr c_free_exp_client cacClient - -getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) -getApplicableVariants client query toss = do - context <- newCAString query - variants <- withForeignPtr client (\c -> c_get_applicable_variants c context (fromInteger toss)) - _ <- cleanup [context] - if variants == nullPtr - then Left <$> getError - else do - fptrVariants <- newForeignPtr c_free_string variants - Right <$> withForeignPtr fptrVariants peekCAString - -- pure $ - -- case fromJSON variantVector of - -- Error s -> Left s - -- Success vec -> Right vec - -getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) -getSatisfiedExperiments client query = do - context <- newCAString query - experiments <- withForeignPtr client (`c_get_satisfied_experiments` context) - _ <- cleanup [context] - if experiments == nullPtr - then Left <$> getError - else do - fptrExperiments <- newForeignPtr c_free_string experiments - Right . toJSON <$> withForeignPtr fptrExperiments peekCAString - -getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) -getRunningExperiments client = do - experiments <- withForeignPtr client c_get_running_experiments - if experiments == nullPtr - then Left <$> getError - else do - fptrExperiments <- newForeignPtr c_free_string experiments - Right . toJSON <$> withForeignPtr fptrExperiments peekCAString diff --git a/clients/hs-exp-client/src/Main.hs b/clients/hs-exp-client/src/Main.hs deleted file mode 100644 index bb7621266..000000000 --- a/clients/hs-exp-client/src/Main.hs +++ /dev/null @@ -1,34 +0,0 @@ -{-# LANGUAGE LambdaCase #-} -module Main (main) where - -import Client (createExpClient, expStartPolling, - getApplicableVariants, getExpClient, - getRunningExperiments, - getSatisfiedExperiments) -import Control.Concurrent -import Prelude - -main :: IO () -main = do - createExpClient "dev" 10 "http://localhost:8080" >>= \case - Left err -> putStrLn err - Right _ -> pure () - threadId <- forkIO (expStartPolling "dev") - print threadId - getExpClient "dev" >>= \case - Left err -> putStrLn err - Right client -> loop client - pure () - where - loop client = do - runningExperiments <- getRunningExperiments client - satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" - variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 - print "Running experiments" - print runningExperiments - print "experiments that satisfy context" - print satisfiedExperiments - print "variant ID applied" - print variants - -- threadDelay 10000000 - loop client diff --git a/crates/cac_client/default.nix b/crates/cac_client/default.nix deleted file mode 100644 index b0c2af779..000000000 --- a/crates/cac_client/default.nix +++ /dev/null @@ -1,63 +0,0 @@ -# Nix for Rust project management -{ inputs, ... }: { - perSystem = { config, self', pkgs, lib, system, ... }: - let - rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = [ - "rust-src" - "rust-analyzer" - "clippy" - ]; - }; - craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; - args = { - pname = "cac_client"; - src = ./.; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cargoVendorDir = craneLib.vendorCargoDeps { - src = ./../../.; - }; - cargoArtifacts = craneLib.buildDepsOnly args; - package = craneLib.buildPackage (args // { - inherit cargoArtifacts; - doCheck = false; # FIXME: tests require services to be running - }); - - check = craneLib.cargoClippy (args // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; - }); - in - { - packages.cac_client = package; - - checks.clippy = check; - - # # Flake outputs - # devShells.rust-exp = pkgs.mkShell { - # inputsFrom = [ - # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) - # ]; - # shellHook = '' - # # For rust-analyzer 'hover' tooltips to work. - # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; - # ''; - # nativeBuildInputs = with pkgs; [ - # # Add your dev tools here. - # rustToolchain - # cargo-watch - # ]; - # }; - }; -} diff --git a/crates/cac_client/rust-toolchain.toml b/crates/cac_client/rust-toolchain.toml deleted file mode 100644 index 31578d3bf..000000000 --- a/crates/cac_client/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" \ No newline at end of file diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs index 6cd391ae0..a8676f480 100644 --- a/crates/experimentation-platform/src/db/schema.rs +++ b/crates/experimentation-platform/src/db/schema.rs @@ -616,4 +616,4 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m11, event_log_y2026m12, experiments, -); \ No newline at end of file +); diff --git a/crates/superposition_client/default.nix b/crates/superposition_client/default.nix deleted file mode 100644 index a78ee5e5d..000000000 --- a/crates/superposition_client/default.nix +++ /dev/null @@ -1,60 +0,0 @@ -# Nix for Rust project management -{ inputs, ... }: { - perSystem = { config, self', pkgs, lib, system, ... }: - let - rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = [ - "rust-src" - "rust-analyzer" - "clippy" - ]; - }; - craneLib = (inputs.common.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; - args = { - pname = "superposition_client"; - src = ./.; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cargoArtifacts = craneLib.buildDepsOnly args; - package = craneLib.buildPackage (args // { - inherit cargoArtifacts; - doCheck = false; # FIXME: tests require services to be running - }); - - check = craneLib.cargoClippy (args // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; - }); - in - { - packages.superposition_client = package; - - checks.clippy = check; - - # # Flake outputs - # devShells.rust-exp = pkgs.mkShell { - # inputsFrom = [ - # package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) - # ]; - # shellHook = '' - # # For rust-analyzer 'hover' tooltips to work. - # export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; - # ''; - # nativeBuildInputs = with pkgs; [ - # # Add your dev tools here. - # rustToolchain - # cargo-watch - # ]; - # }; - }; -} diff --git a/crates/superposition_client/rust-toolchain.toml b/crates/superposition_client/rust-toolchain.toml deleted file mode 100644 index 31578d3bf..000000000 --- a/crates/superposition_client/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" \ No newline at end of file diff --git a/crates/superposition_client/src/lib.rs b/crates/superposition_client/src/lib.rs index df009f7bb..dbd3ffd93 100644 --- a/crates/superposition_client/src/lib.rs +++ b/crates/superposition_client/src/lib.rs @@ -1,6 +1,7 @@ mod interface; mod types; mod interface; +mod types; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; diff --git a/flake.lock b/flake.lock index 4dc7a1e83..02944e91f 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1705974079, - "narHash": "sha256-HyC3C2esW57j6bG0MKwX4kQi25ltslRnr6z2uvpadJo=", + "lastModified": 1707461758, + "narHash": "sha256-VaqINICYEtVKF0X+chdNtXcNp6poZr385v6AG7j0ybM=", "owner": "ipetkov", "repo": "crane", - "rev": "0b4e511fe6e346381e31d355e03de52aa43e8cb2", + "rev": "505976eaeac289fe41d074bee37006ac094636bb", "type": "github" }, "original": { @@ -25,11 +25,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1704982712, - "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", + "lastModified": 1706830856, + "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "07f6395285469419cf9d078f59b5b49993198c00", + "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", "type": "github" }, "original": { @@ -58,11 +58,11 @@ }, "haskell-flake": { "locked": { - "lastModified": 1705984248, - "narHash": "sha256-1zW2udPF4clKCuTgk0FHjGUB7x/Tfe8m9oUhe3hM/YQ=", + "lastModified": 1707242163, + "narHash": "sha256-w+cBynh7yqnpVtFdu1SEZxPgtlz/nWnv47D5crnPXHM=", "owner": "srid", "repo": "haskell-flake", - "rev": "cea3332b027264e914673df392870ebb1e92de44", + "rev": "f9d17c3aa68e65529f424816c8b9346ae602d1de", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1706550542, - "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "lastModified": 1707268954, + "narHash": "sha256-2en1kvde3cJVc3ZnTy8QeD2oKcseLFjYPLKhIGDanQ0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1", "type": "github" }, "original": { @@ -88,24 +88,6 @@ } }, "nixpkgs-lib": { - "locked": { - "dir": "lib", - "lastModified": 1703961334, - "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", - "type": "github" - }, - "original": { - "dir": "lib", - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { "locked": { "dir": "lib", "lastModified": 1706550542, @@ -129,13 +111,16 @@ "flake-parts": "flake-parts", "haskell-flake": "haskell-flake", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "rust-overlay": "rust-overlay", + "systems": "systems_2" } }, "rust-overlay": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { "lastModified": 1707444620, @@ -165,6 +150,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 33fa4d909..fa16e1000 100644 --- a/flake.nix +++ b/flake.nix @@ -1,124 +1,45 @@ { inputs = { - flake-parts.url = "github:hercules-ci/flake-parts"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - crane.url = "github:ipetkov/crane"; - crane.inputs.nixpkgs.follows = "nixpkgs"; + flake-parts.url = "github:hercules-ci/flake-parts"; haskell-flake.url = "github:srid/haskell-flake"; - rust-overlay = { url = "github:oxalica/rust-overlay"; }; + systems.url = "github:nix-systems/default"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = inputs@{ self, rust-overlay, flake-parts, crane, nixpkgs, haskell-flake, ... }: + outputs = inputs: - flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ]; + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; imports = [ inputs.haskell-flake.flakeModule - ./clients/hs-cac-client - ./clients/hs-exp-client + ./clients/haskell + ./rust.nix ]; - perSystem = { config, self', inputs', system, pkgs, lib, haskell-flake, ... }: - let - craneLib = crane.lib.${system}; - cac_args = { - pname = "cac_client"; - src = ./crates/cac_client; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - exp_args = { - pname = "superposition_client"; - src = ./crates/superposition_client; - buildInputs = lib.optionals pkgs.stdenv.isDarwin - (with pkgs.darwin.apple_sdk.frameworks; [ - Security - SystemConfiguration - ]) ++ [ - pkgs.libiconv - pkgs.openssl - ]; - nativeBuildInputs = [ - pkgs.pkg-config - ]; - }; - cacCargoArtifacts = craneLib.buildDepsOnly cac_args; - expCargoArtifacts = craneLib.buildDepsOnly exp_args; - cac_client = craneLib.buildPackage (cac_args // { - inherit cacCargoArtifacts; - }); - superposition_client = craneLib.buildPackage (exp_args // { - inherit expCargoArtifacts; - }); - in - rec { - _module.args.pkgs = import nixpkgs { - inherit system; - overlays = [ rust-overlay.overlay ]; - - }; - formatter = pkgs.nixpkgs-fmt; - # For `nix build` & `nix run`: - - packages.clients = craneLib.buildPackage { - nativeBuildInputs = with pkgs; [ postgresql_12 iconv darwin.apple_sdk.frameworks.Security ]; - src = craneLib.path ./.; # FIXME: move rust stuff to subdir - }; - - # For `nix develop`: - devShells.default = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - # bring your local shell properties into nix env - # shellHook = '' - # echo "you are now in the nix shell" - # eval $($SHELL) - # ''; - nativeBuildInputs = - let - univPkgs = with pkgs; [ - # Build requirements - cargo - libiconv - openssl - postgresql_12 - # Extras - rust-analyzer - rustfmt - bacon - cargo-watch - clippy - diesel-cli - docker-compose - stdenv.cc - pkg-config - awscli - jq - pkgs.nodejs_18 - leptosfmt - wasm-pack - curl - (rust-bin.stable.latest.default.override { - extensions = [ "rust-src" ]; - targets = [ "wasm32-unknown-unknown" ]; - }) - ]; - darwinPkgs = with pkgs; [ - darwin.apple_sdk.frameworks.Security - darwin.apple_sdk.frameworks.SystemConfiguration - ]; - in - univPkgs ++ (if pkgs.stdenv.isDarwin then darwinPkgs else [ ]); - }; + perSystem = { pkgs, self', ... }: { + devShells.default = pkgs.mkShell { + inputsFrom = [ + self'.devShells.rust + ]; + packages = with pkgs; [ + docker-compose + # Why do we need this? + stdenv.cc + awscli + jq + nodejs_18 + nixpkgs-fmt + ]; }; + }; }; -} +} \ No newline at end of file diff --git a/libs b/libs deleted file mode 120000 index 6e0cd6fa6..000000000 --- a/libs +++ /dev/null @@ -1 +0,0 @@ -/nix/store/wap6n7b93hm0vnwn4kx8yafxb4yl7lwc-hs-cac-client-0.1.0.0 \ No newline at end of file From 4e2be481910778c691ea4e95b02894a691a89289 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 16 Feb 2024 16:03:24 +0530 Subject: [PATCH 324/352] chore: rename superposition to experimentation --- CHANGELOG.md | 16 ++++----- Cargo.lock | 36 +++++++++---------- Cargo.toml | 2 +- cog.toml | 2 +- .../CHANGELOG.md | 14 ++++---- .../Cargo.toml | 2 +- .../README.md | 0 .../build.rs | 0 .../src/interface.rs | 0 .../src/lib.rs | 0 .../src/types.rs | 0 .../Cargo.toml | 2 +- .../src/main.rs | 2 +- docs/client-experimentation.md | 6 ++-- 14 files changed, 41 insertions(+), 41 deletions(-) rename crates/{superposition_client => experimentation_client}/CHANGELOG.md (85%) rename crates/{superposition_client => experimentation_client}/Cargo.toml (94%) rename crates/{superposition_client => experimentation_client}/README.md (100%) rename crates/{superposition_client => experimentation_client}/build.rs (100%) rename crates/{superposition_client => experimentation_client}/src/interface.rs (100%) rename crates/{superposition_client => experimentation_client}/src/lib.rs (100%) rename crates/{superposition_client => experimentation_client}/src/types.rs (100%) rename crates/{superposition_client_integration_example => experimentation_client_integration_example}/Cargo.toml (83%) rename crates/{superposition_client_integration_example => experimentation_client_integration_example}/src/main.rs (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce238391..89ac53a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -270,7 +270,7 @@ All notable changes to this project will be documented in this file. See [conven - cac_client locked to cac_client-v0.5.0 - external locked to external-v0.3.0 - service-utils locked to service-utils-v0.10.2 -- superposition_client locked to superposition_client-v0.4.0 +- experimentation_client locked to experimentation_client-v0.4.0 - context-aware-config locked to context-aware-config-v0.14.2 ### Global changes #### Bug Fixes @@ -420,7 +420,7 @@ All notable changes to this project will be documented in this file. See [conven - external bumped to external-v0.3.0 - cac_client bumped to cac_client-v0.4.0 - service-utils bumped to service-utils-v0.9.0 -- superposition_client bumped to superposition_client-v0.4.0 +- experimentation_client bumped to experimentation_client-v0.4.0 - experimentation-platform bumped to experimentation-platform-v0.8.0 ### Global changes #### Bug Fixes @@ -474,7 +474,7 @@ All notable changes to this project will be documented in this file. See [conven - context-aware-config bumped to context-aware-config-v0.10.1 - cac_client bumped to cac_client-v0.3.0 - service-utils bumped to service-utils-v0.7.1 -- superposition_client bumped to superposition_client-v0.3.0 +- experimentation_client bumped to experimentation_client-v0.3.0 ### Global changes #### Bug Fixes - fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev @@ -501,14 +501,14 @@ All notable changes to this project will be documented in this file. See [conven ### Package updates - context-aware-config bumped to context-aware-config-v0.9.0 - service-utils bumped to service-utils-v0.6.0 -- superposition_client bumped to superposition_client-v0.2.0 +- experimentation_client bumped to experimentation_client-v0.2.0 ### Global changes - - - ## v0.9.1 - 2023-10-13 ### Package updates -- superposition_client bumped to superposition_client-v0.1.3 +- experimentation_client bumped to experimentation_client-v0.1.3 ### Global changes - - - @@ -594,7 +594,7 @@ All notable changes to this project will be documented in this file. See [conven ## v0.5.1 - 2023-09-06 ### Package updates -- superposition_client bumped to superposition_client-v0.1.2 +- experimentation_client bumped to experimentation_client-v0.1.2 ### Global changes #### Bug Fixes - trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev @@ -614,7 +614,7 @@ All notable changes to this project will be documented in this file. See [conven ## v0.4.1 - 2023-09-06 ### Package updates -- superposition_client bumped to superposition_client-v0.1.1 +- experimentation_client bumped to experimentation_client-v0.1.1 ### Global changes #### Bug Fixes - fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev @@ -664,7 +664,7 @@ All notable changes to this project will be documented in this file. See [conven - service-utils bumped to service-utils-v0.1.0 - cac_client bumped to cac_client-v0.1.0 - context-aware-config bumped to context-aware-config-v0.1.0 -- superposition_client bumped to superposition_client-v0.1.0 +- experimentation_client bumped to experimentation_client-v0.1.0 ### Global changes #### Bug Fixes - PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan diff --git a/Cargo.lock b/Cargo.lock index 470177b39..305cc9067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,8 +1392,8 @@ dependencies = [ "actix", "actix-web", "chrono", + "experimentation_client", "serde_json", - "superposition_client", ] [[package]] @@ -1418,6 +1418,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "experimentation_client" +version = "0.4.0" +dependencies = [ + "cbindgen", + "chrono", + "derive_more", + "dotenv", + "jsonlogic", + "log", + "once_cell", + "reqwest", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "external" version = "0.4.0" @@ -3694,23 +3711,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "superposition_client" -version = "0.5.0" -dependencies = [ - "cbindgen", - "chrono", - "derive_more", - "dotenv", - "jsonlogic", - "log", - "once_cell", - "reqwest", - "serde", - "serde_json", - "tokio", -] - [[package]] name = "sval" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 4f243cd64..f2fd796d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" members = [ "crates/service-utils", "crates/context-aware-config", - "crates/superposition_client", + "crates/experimentation_client", "crates/cac_client", "crates/superposition_client_integration_example", "crates/frontend", diff --git a/cog.toml b/cog.toml index 51355045e..ae80e8d15 100644 --- a/cog.toml +++ b/cog.toml @@ -32,4 +32,4 @@ external = { path = "crates/external" } frontend = { path = "crates/frontend" } caclang = { path = "crates/caclang" } cac_client = { path = "crates/cac_client" } -superposition_client = { path = "crates/superposition_client" } \ No newline at end of file +experimentation_client = { path = "crates/experimentation_client" } \ No newline at end of file diff --git a/crates/superposition_client/CHANGELOG.md b/crates/experimentation_client/CHANGELOG.md similarity index 85% rename from crates/superposition_client/CHANGELOG.md rename to crates/experimentation_client/CHANGELOG.md index 72d80be3a..ba9eb311e 100644 --- a/crates/superposition_client/CHANGELOG.md +++ b/crates/experimentation_client/CHANGELOG.md @@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file. See [conven - - - -## superposition_client-v0.4.0 - 2023-11-11 +## experimentation_client-v0.4.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman #### Miscellaneous Chores @@ -18,31 +18,31 @@ All notable changes to this project will be documented in this file. See [conven - - - -## superposition_client-v0.3.0 - 2023-10-27 +## experimentation_client-v0.3.0 - 2023-10-27 #### Features - multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev - - - -## superposition_client-v0.2.0 - 2023-10-20 +## experimentation_client-v0.2.0 - 2023-10-20 #### Features - PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P - - - -## superposition_client-v0.1.3 - 2023-10-13 +## experimentation_client-v0.1.3 - 2023-10-13 #### Bug Fixes - PICAF-24612 add all variants in manifest - (0f15ac9) - Pratik Mishra - - - -## superposition_client-v0.1.2 - 2023-09-06 +## experimentation_client-v0.1.2 - 2023-09-06 #### Bug Fixes - trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev - - - -## superposition_client-v0.1.1 - 2023-09-06 +## experimentation_client-v0.1.1 - 2023-09-06 #### Bug Fixes - fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev #### Continuous Integration @@ -50,7 +50,7 @@ All notable changes to this project will be documented in this file. See [conven - - - -## superposition_client-v0.1.0 - 2023-09-01 +## experimentation_client-v0.1.0 - 2023-09-01 #### Bug Fixes - PICAF-24114 removed unwanted parameter to prevent warning - (3de7fe7) - Ritick Madaan - PICAF-24114 allowing cug users to fall under test variants - (c095333) - Ritick Madaan diff --git a/crates/superposition_client/Cargo.toml b/crates/experimentation_client/Cargo.toml similarity index 94% rename from crates/superposition_client/Cargo.toml rename to crates/experimentation_client/Cargo.toml index ecb21cb3f..538fa5b29 100644 --- a/crates/superposition_client/Cargo.toml +++ b/crates/experimentation_client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "superposition_client" +name = "experimentation_client" version = "0.5.0" edition = "2021" diff --git a/crates/superposition_client/README.md b/crates/experimentation_client/README.md similarity index 100% rename from crates/superposition_client/README.md rename to crates/experimentation_client/README.md diff --git a/crates/superposition_client/build.rs b/crates/experimentation_client/build.rs similarity index 100% rename from crates/superposition_client/build.rs rename to crates/experimentation_client/build.rs diff --git a/crates/superposition_client/src/interface.rs b/crates/experimentation_client/src/interface.rs similarity index 100% rename from crates/superposition_client/src/interface.rs rename to crates/experimentation_client/src/interface.rs diff --git a/crates/superposition_client/src/lib.rs b/crates/experimentation_client/src/lib.rs similarity index 100% rename from crates/superposition_client/src/lib.rs rename to crates/experimentation_client/src/lib.rs diff --git a/crates/superposition_client/src/types.rs b/crates/experimentation_client/src/types.rs similarity index 100% rename from crates/superposition_client/src/types.rs rename to crates/experimentation_client/src/types.rs diff --git a/crates/superposition_client_integration_example/Cargo.toml b/crates/experimentation_client_integration_example/Cargo.toml similarity index 83% rename from crates/superposition_client_integration_example/Cargo.toml rename to crates/experimentation_client_integration_example/Cargo.toml index 83ad91dd4..cb5938bb3 100644 --- a/crates/superposition_client_integration_example/Cargo.toml +++ b/crates/experimentation_client_integration_example/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -superposition_client = { path = "../superposition_client" } +experimentation_client = { path = "../experimentation_client" } chrono = { workspace = true } # Https server framework diff --git a/crates/superposition_client_integration_example/src/main.rs b/crates/experimentation_client_integration_example/src/main.rs similarity index 97% rename from crates/superposition_client_integration_example/src/main.rs rename to crates/experimentation_client_integration_example/src/main.rs index 8a1bae13c..ebc4e5247 100644 --- a/crates/superposition_client_integration_example/src/main.rs +++ b/crates/experimentation_client_integration_example/src/main.rs @@ -5,7 +5,7 @@ use actix_web::{ }; use serde_json::json; -use superposition_client as exp; +use experimentation_client as exp; #[actix_web::main] async fn main() -> std::io::Result<()> { diff --git a/docs/client-experimentation.md b/docs/client-experimentation.md index c2d8ba34a..fadcc07a1 100644 --- a/docs/client-experimentation.md +++ b/docs/client-experimentation.md @@ -1,6 +1,6 @@ # Experimentation Client Integration -This provides SDK to interact with ```experimentation-platform```.We support superposition_client for +This provides SDK to interact with ```experimentation-platform```.We support experimentation_client for 1. Rust 2. Haskell @@ -23,7 +23,7 @@ for tenant in tenants { hostname.to_string()// superposition hostname ) .await - .expect(format!("{}: Failed to acquire superposition_client", tenant).as_str()) + .expect(format!("{}: Failed to acquire experimentation_client", tenant).as_str()) .clone() .run_polling_updates(), ); @@ -35,7 +35,7 @@ let sp_client = sp::CLIENT_FACTORY .await .map_err(|e| { log::error!("{}: {}", tenant, e); - ErrorType::IgnoreError(format!("{}: Failed to get superposition_client", tenant)) + ErrorType::IgnoreError(format!("{}: Failed to get experimentation_client", tenant)) })?; From 659d3845743030f830de43f47039071c310ecea8 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Tue, 20 Feb 2024 19:33:11 +0530 Subject: [PATCH 325/352] feat: client interface improvements - Unified interfaces to create CAC and experimentation clients - Removed thread runtime dependency is CAC client - Added new methods to get specific keys in resolved and default configs --- .gitignore | 3 +- Cargo.lock | 2 +- Cargo.toml | 5 +- clients/haskell/default.nix | 8 +- clients/haskell/hs-cac-client/src/Client.hs | 55 ++- clients/haskell/hs-cac-client/src/Main.hs | 12 +- .../haskell/hs-exp-client/hs-exp-client.cabal | 4 +- crates/cac_client/build.rs | 4 +- crates/cac_client/src/interface.rs | 244 ++++++------ crates/cac_client/src/lib.rs | 89 +++-- crates/experimentation_client/CHANGELOG.md | 2 +- crates/experimentation_client/Cargo.toml | 2 +- crates/experimentation_client/build.rs | 2 +- crates/experimentation_client/src/lib.rs | 2 - .../src/main.rs | 2 +- docs/client-context-aware-configuration.md | 352 +++++++++++++++++- docs/client-experimentation.md | 266 ++++++++++++- flake.nix | 3 +- headers/libcac_client.h | 14 +- ...n_client.h => libexperimentation_client.h} | 20 - 20 files changed, 859 insertions(+), 232 deletions(-) rename headers/{libsuperposition_client.h => libexperimentation_client.h} (54%) diff --git a/.gitignore b/.gitignore index d975e813d..15b716990 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,8 @@ backend/.env .direnv /result /result-* - +clients/haskell/result +clients/haskell/dist-newstyle # dev bacon.toml docker-compose/localstack/export_cyphers.sh diff --git a/Cargo.lock b/Cargo.lock index 305cc9067..6785f2a91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,7 +1420,7 @@ dependencies = [ [[package]] name = "experimentation_client" -version = "0.4.0" +version = "0.5.0" dependencies = [ "cbindgen", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f2fd796d4..f8ec0c4af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,12 @@ resolver = "2" members = [ "crates/service-utils", "crates/context-aware-config", + "crates/experimentation-platform", + "crates/external", + "crates/service-utils", "crates/experimentation_client", "crates/cac_client", - "crates/superposition_client_integration_example", + "crates/experimentation_client_integration_example", "crates/frontend", "crates/caclang" ] diff --git a/clients/haskell/default.nix b/clients/haskell/default.nix index ff3ea7d2b..1e18a8491 100644 --- a/clients/haskell/default.nix +++ b/clients/haskell/default.nix @@ -5,18 +5,18 @@ autoWire = [ "packages" "checks" "apps" ]; settings = { cac_client.custom = _: self'.packages.superposition; - superposition_client.custom = _: self'.packages.superposition; + experimentation_client.custom = _: self'.packages.superposition; }; }; devShells.haskell = pkgs.mkShell { name = "superposition-haskell-clients"; - inputsFrom = [ - config.haskellProjects.default.outputs.devShell - ]; shellHook = '' export LIBRARY_PATH=${self'.packages.superposition}/lib ''; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + ]; }; }; } diff --git a/clients/haskell/hs-cac-client/src/Client.hs b/clients/haskell/hs-cac-client/src/Client.hs index eb0aaefaa..e0e4cde62 100644 --- a/clients/haskell/hs-cac-client/src/Client.hs +++ b/clients/haskell/hs-cac-client/src/Client.hs @@ -6,10 +6,12 @@ module Client ( createCacClient , getCacClient -, getCacConfig +, getFullConfigState , getCacLastModified -, cacEval +, getResolvedConfig , cacStartPolling +, getDefaultConfig +, getResolvedConfigWithStrategy ) where import Data.Aeson @@ -18,6 +20,8 @@ import Foreign.C.String (CString, newCAString, peekCAString) import Foreign.C.Types (CInt (CInt), CULong (..)) import Foreign.ForeignPtr import Foreign.Marshal.Alloc (free) +import Foreign.Marshal.Array (withArrayLen) +import Data.List (intercalate) import Foreign.Ptr import Prelude @@ -31,7 +35,7 @@ type Tenant = String type Error = String foreign import ccall unsafe "new_client" - c_new_cac_client :: CTenant -> Bool -> CULong -> CString -> IO CInt + c_new_cac_client :: CTenant -> CULong -> CString -> IO CInt foreign import ccall unsafe "&free_client" c_free_cac_client :: FunPtr (Ptr CacClient -> IO ()) @@ -48,8 +52,11 @@ foreign import ccall unsafe "get_last_modified" foreign import ccall unsafe "get_config" c_get_config :: Ptr CacClient -> IO CString -foreign import ccall unsafe "cac_eval" - c_cac_eval :: Ptr CacClient -> CString -> IO CString +foreign import ccall unsafe "get_resolved_config" + c_cac_get_resolved_config :: Ptr CacClient -> CString -> CString -> CString -> IO CString + +foreign import ccall unsafe "get_default_config" + c_cac_get_default_config :: Ptr CacClient -> CString -> IO CString foreign import ccall safe "start_polling_update" c_cac_poll :: CTenant -> IO () @@ -57,6 +64,8 @@ foreign import ccall safe "start_polling_update" foreign import ccall unsafe "&free_string" c_free_string :: FunPtr (CString -> IO ()) +data MergeStrategy = MERGE | REPLACE deriving (Show, Eq, Ord, Enum) + cacStartPolling :: Tenant -> IO () cacStartPolling tenant = newCAString tenant @@ -71,12 +80,12 @@ getError = c_last_error_message cleanup :: [Ptr a] -> IO () cleanup items = mapM free items $> () -createCacClient:: Tenant -> Bool -> Integer -> String -> IO (Either Error ()) -createCacClient tenant shouldUpdate frequency hostname = do +createCacClient:: Tenant -> Integer -> String -> IO (Either Error ()) +createCacClient tenant frequency hostname = do let duration = fromInteger frequency cTenant <- newCAString tenant cHostname <- newCAString hostname - resp <- c_new_cac_client cTenant shouldUpdate duration cHostname + resp <- c_new_cac_client cTenant duration cHostname _ <- cleanup [cTenant, cHostname] case resp of 0 -> pure $ Right () @@ -91,8 +100,8 @@ getCacClient tenant = do then Left <$> getError else Right <$> newForeignPtr c_free_cac_client cacClient -getCacConfig :: ForeignPtr CacClient -> IO (Either Error Value) -getCacConfig client = do +getFullConfigState :: ForeignPtr CacClient -> IO (Either Error Value) +getFullConfigState client = do config <- withForeignPtr client c_get_config if config == nullPtr then Left <$> getError @@ -109,13 +118,29 @@ getCacLastModified client = do fptrLastModified <- newForeignPtr c_free_string lastModified Right <$> withForeignPtr fptrLastModified peekCAString -cacEval :: ForeignPtr CacClient -> String -> IO (Either Error Value) -cacEval client context = do - cContext <- newCAString context - overrides <- withForeignPtr client (`c_cac_eval` cContext) - _ <- cleanup [cContext] +getResolvedConfigWithStrategy :: ForeignPtr CacClient -> String -> [String] -> MergeStrategy -> IO (Either Error Value) +getResolvedConfigWithStrategy client context keys mergeStrat = do + cContext <- newCAString context + cMergeStrat <- newCAString (show mergeStrat) + cStrKeys <- newCAString (intercalate "|" keys) + overrides <- withForeignPtr client $ \client -> c_cac_get_resolved_config client cContext cStrKeys cMergeStrat + _ <- cleanup [cContext, cStrKeys] if overrides == nullPtr then Left <$> getError else do fptrOverrides <- newForeignPtr c_free_string overrides Right . toJSON <$> withForeignPtr fptrOverrides peekCAString + +getDefaultConfig :: ForeignPtr CacClient -> [String] -> IO (Either Error Value) +getDefaultConfig client keys = do + cStrKeys <- newCAString (intercalate "|" keys) + overrides <- withForeignPtr client $ \client -> c_cac_get_default_config client cStrKeys + _ <- cleanup [cStrKeys] + if overrides == nullPtr + then Left <$> getError + else do + fptrOverrides <- newForeignPtr c_free_string overrides + Right . toJSON <$> withForeignPtr fptrOverrides peekCAString + +getResolvedConfig :: ForeignPtr CacClient -> String -> [String] -> IO (Either Error Value) +getResolvedConfig client context keys = getResolvedConfigWithStrategy client context keys MERGE diff --git a/clients/haskell/hs-cac-client/src/Main.hs b/clients/haskell/hs-cac-client/src/Main.hs index d3530ab1d..81bea9ee4 100644 --- a/clients/haskell/hs-cac-client/src/Main.hs +++ b/clients/haskell/hs-cac-client/src/Main.hs @@ -1,14 +1,14 @@ {-# LANGUAGE LambdaCase #-} module Main (main) where -import Client (cacEval, createCacClient, getCacClient, - getCacConfig, getCacLastModified, cacStartPolling) +import Client (getResolvedConfig, createCacClient, getCacClient, + getFullConfigState, getCacLastModified, cacStartPolling, getDefaultConfig) import Control.Concurrent import Prelude main :: IO () main = do - createCacClient "dev" True 10 "http://localhost:8080" >>= \case + createCacClient "dev" 10 "http://localhost:8080" >>= \case Left err -> putStrLn err Right _ -> pure () threadId <- forkOS (cacStartPolling "dev") @@ -16,11 +16,13 @@ main = do getCacClient "dev" >>= \case Left err -> putStrLn err Right client -> do - config <- getCacConfig client + config <- getFullConfigState client lastModified <- getCacLastModified client - overrides <- cacEval client "{\"os\": \"android\", \"client\": \"2mg\"}" + overrides <- getResolvedConfig client "{\"country\": \"India\"}" ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client ["country_image_url", "hyperpay_version"] print config print lastModified print overrides + print defaults threadDelay 1000000000 pure () diff --git a/clients/haskell/hs-exp-client/hs-exp-client.cabal b/clients/haskell/hs-exp-client/hs-exp-client.cabal index 7fe61b7a9..6418689fe 100644 --- a/clients/haskell/hs-exp-client/hs-exp-client.cabal +++ b/clients/haskell/hs-exp-client/hs-exp-client.cabal @@ -65,7 +65,7 @@ library BlockArguments ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded extra-libraries: - superposition_client + experimentation_client build-depends: aeson , base >=4.7 && <5 @@ -112,7 +112,7 @@ executable hs-exp-client-exe BlockArguments ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N extra-libraries: - superposition_client + experimentation_client build-depends: aeson , base >=4.7 && <5 diff --git a/crates/cac_client/build.rs b/crates/cac_client/build.rs index 5f6fd1d0b..cb9d3c4d2 100644 --- a/crates/cac_client/build.rs +++ b/crates/cac_client/build.rs @@ -6,6 +6,6 @@ fn main() { config.language = cbindgen::Language::C; println!("Calling build.rs in cac_client"); cbindgen::generate_with_config(&crate_dir, config) - .unwrap() - .write_to_file("../../headers/libcac_client.h"); + .unwrap() + .write_to_file("../../headers/libcac_client.h"); } diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs index eaffee304..4686ed820 100644 --- a/crates/cac_client/src/interface.rs +++ b/crates/cac_client/src/interface.rs @@ -10,7 +10,6 @@ use serde_json::{Map, Value}; use std::{ cell::RefCell, ffi::{c_int, CString}, - str::FromStr, time::Duration, }; use tokio::{runtime::Runtime, task}; @@ -19,7 +18,33 @@ thread_local! { static LAST_ERROR: RefCell<Option<Box<String>>> = RefCell::new(None); } +macro_rules! null_check { + ($client: ident, $err: literal, $return: stmt) => { + if $client.is_null() { + update_last_error($err.into()); + $return + } + }; +} + +macro_rules! unwrap_safe { + ($result: expr, $return: stmt) => { + match $result { + Ok(value) => value, + Err(err) => { + update_last_error(err.to_string()); + $return + } + } + }; +} + fn cstring_to_rstring(s: *const c_char) -> Result<String, String> { + null_check!( + s, + "Invalid C string passed: string was a NULL pointer", + return Err("Invalid C string passed: string was a NULL pointer".into()) + ); let s = unsafe { CStr::from_ptr(s) }; s.to_str().map(str::to_string).map_err_to_string() } @@ -30,7 +55,6 @@ fn rstring_to_cstring(s: String) -> CString { pub fn update_last_error(err: String) { println!("Setting LAST_ERROR: {}", err); - LAST_ERROR.with(|prev| { *prev.borrow_mut() = Some(Box::new(err)); }); @@ -50,10 +74,10 @@ pub extern "C" fn last_error_length() -> c_int { #[no_mangle] pub unsafe extern "C" fn last_error_message() -> *const c_char { - let last_error = match take_last_error() { - Some(err) => err, - None => return std::ptr::null_mut(), - }; + let last_error = unwrap_safe!( + take_last_error().ok_or("No error found"), + return std::ptr::null_mut() + ); let error_message = last_error.to_string(); // println!("Error in last_error_message {error_message}"); let err = rstring_to_cstring(error_message); @@ -73,31 +97,18 @@ pub unsafe extern "C" fn free_string(s: *mut c_char) { #[no_mangle] pub extern "C" fn new_client( tenant: *const c_char, - update_periodically: bool, update_frequency: c_ulong, hostname: *const c_char, ) -> c_int { let duration = Duration::new(update_frequency, 0); - let tenant = match cstring_to_rstring(tenant) { - Ok(value) => value, - Err(err) => { - update_last_error(err); - return 1; - } - }; - let hostname = match cstring_to_rstring(hostname) { - Ok(value) => value, - Err(err) => { - update_last_error(err); - return 1; - } - }; + let tenant = unwrap_safe!(cstring_to_rstring(tenant), return 1); + let hostname = unwrap_safe!(cstring_to_rstring(hostname), return 1); // println!("Creating cac client thread for tenant {tenant}"); let local = task::LocalSet::new(); local.block_on(&Runtime::new().unwrap(), async move { match CLIENT_FACTORY - .create_client(tenant.clone(), update_periodically, duration, hostname) + .create_client(tenant.clone(), duration, hostname) .await { Ok(_) => return 0, @@ -112,16 +123,15 @@ pub extern "C" fn new_client( #[no_mangle] pub extern "C" fn start_polling_update(tenant: *const c_char) { - if tenant.is_null() { - return (); - } + null_check!(tenant, "NULL pointer provided for tenant", return ()); unsafe { let client = get_client(tenant); + null_check!(client, "CAC client for tenant not found", return ()); let local = task::LocalSet::new(); // println!("in FFI polling"); local.block_on( &Runtime::new().unwrap(), - (**client).clone().ffi_polling_update(), + (*client).clone().run_polling_updates(), ); } } @@ -138,97 +148,117 @@ pub extern "C" fn free_client(ptr: *mut Arc<Client>) { #[no_mangle] pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc<Client> { - let ten = match cstring_to_rstring(tenant) { - Ok(t) => t, - Err(err) => { - update_last_error(err); - return std::ptr::null_mut(); - } - }; + let ten = unwrap_safe!(cstring_to_rstring(tenant), return std::ptr::null_mut()); // println!("fetching cac client thread for tenant {ten}"); - match CLIENT_FACTORY.get_client(ten).map_err(|e| format!("{}", e)) { - Ok(client) => Box::into_raw(Box::new(client)), - Err(err) => { - // println!("error occurred {err}"); - update_last_error(err); - // println!("error set"); - std::ptr::null_mut() - } - } + unwrap_safe!( + CLIENT_FACTORY + .get_client(ten) + .map(|client| Box::into_raw(Box::new(client))), + std::ptr::null_mut() + ) } #[no_mangle] -pub extern "C" fn cac_eval( - client: *mut Arc<Client>, - query: *const c_char, - merge_strategy: *const c_char, -) -> *const c_char { - let context_string = match cstring_to_rstring(query) { - Ok(s) => s, - Err(err) => { - update_last_error(err); - return std::ptr::null(); - } - }; - let context: Map<String, Value> = - match serde_json::from_str::<Map<String, Value>>(context_string.as_str()) { - Ok(json) => json, - Err(err) => { - update_last_error(err.to_string()); - return std::ptr::null(); - } - }; - let merge_strategy = match cstring_to_rstring(merge_strategy) { - Ok(s) => match MergeStrategy::from_str(s.as_str()) { - Ok(strat) => strat, - Err(err) => { - update_last_error(err.to_string()); - return std::ptr::null(); - } +pub extern "C" fn get_last_modified(client: *mut Arc<Client>) -> *const c_char { + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + unwrap_safe!( + unsafe { + (*client) + .get_last_modified() + .map(|date| rstring_to_cstring(date.to_string()).into_raw()) }, - Err(err) => { - update_last_error(err); - return std::ptr::null(); - } - }; - let overrides = match unsafe { (*client).eval(context, merge_strategy) } { - Ok(ov) => match serde_json::to_string::<Map<String, Value>>(&ov) { - Ok(ove) => ove, - Err(err) => { - update_last_error(err.to_string()); - return std::ptr::null(); - } + std::ptr::null() + ) +} + +#[no_mangle] +pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + unwrap_safe!( + unsafe { + (*client).get_full_config_state().map(|config| { + rstring_to_cstring(serde_json::to_value(config).unwrap().to_string()) + .into_raw() + }) }, - Err(err) => { - update_last_error(err); - return std::ptr::null(); - } - }; - rstring_to_cstring(overrides).into_raw() + return std::ptr::null_mut() + ) } #[no_mangle] -pub extern "C" fn get_last_modified(client: *mut Arc<Client>) -> *const c_char { - let last_modified = unsafe { (*client).get_last_modified() }; - match last_modified { - Ok(date) => rstring_to_cstring(date.to_string()).into_raw(), - Err(err) => { - update_last_error(err); - std::ptr::null() - } - } +pub extern "C" fn get_resolved_config( + client: *mut Arc<Client>, + query: *const c_char, + keys: *const c_char, + merge_strategy: *const c_char, +) -> *const c_char { + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + + let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); + let key_vector = key.split("|").map(str::to_string).collect(); + + let query = unwrap_safe!(cstring_to_rstring(query), return std::ptr::null()); + let merge_strategem = + unwrap_safe!(cstring_to_rstring(merge_strategy), return std::ptr::null()); + println!( + "key vector {:#?}, merge strategy {:#?}", + key_vector, merge_strategem + ); + + let context = unwrap_safe!( + serde_json::from_str::<Map<String, Value>>(query.as_str()), + return std::ptr::null() + ); + + unwrap_safe!( + unsafe { + (*client) + .get_resolved_config( + context, + key_vector, + MergeStrategy::from(merge_strategem), + ) + .map(|ov| { + unwrap_safe!( + serde_json::to_string::<Map<String, Value>>(&ov) + .map(|overrides| rstring_to_cstring(overrides).into_raw()), + return std::ptr::null() + ) + }) + }, + return std::ptr::null() + ) } #[no_mangle] -pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { - let config = unsafe { (*client).get_config() }; - match config { - Ok(c) => { - rstring_to_cstring(serde_json::to_value(c).unwrap().to_string()).into_raw() - } - Err(err) => { - update_last_error(err); - std::ptr::null_mut() - } - } +pub extern "C" fn get_default_config( + client: *mut Arc<Client>, + keys: *const c_char, +) -> *const c_char { + let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); + let key_vector = key.split("|").map(str::to_string).collect(); + unwrap_safe!( + unsafe { + (*client).get_default_config(key_vector).map(|ov| { + unwrap_safe!( + serde_json::to_string::<Map<String, Value>>(&ov) + .map(|overrides| rstring_to_cstring(overrides).into_raw()), + return std::ptr::null() + ) + }) + }, + return std::ptr::null() + ) } diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 75b560d59..8a150b826 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -2,10 +2,7 @@ mod eval; mod interface; mod utils; -use actix_web::{ - rt::{self, time::interval}, - web::Data, -}; +use actix_web::{rt::time::interval, web::Data}; use chrono::{DateTime, Utc}; use derive_more::{Deref, DerefMut}; use reqwest::{RequestBuilder, Response, StatusCode}; @@ -47,6 +44,16 @@ impl Default for MergeStrategy { } } +impl From<String> for MergeStrategy { + fn from(value: String) -> Self { + match value.to_lowercase().as_str() { + "replace" => MergeStrategy::REPLACE, + "merge" => MergeStrategy::MERGE, + _ => MergeStrategy::default(), + } + } +} + #[repr(C)] #[derive(Clone)] pub struct Client { @@ -77,7 +84,6 @@ fn get_last_modified(resp: &Response) -> Option<DateTime<Utc>> { impl Client { pub async fn new( tenant: String, - update_config_periodically: bool, polling_interval: Duration, hostname: String, ) -> Result<Self, String> { @@ -101,9 +107,6 @@ impl Client { )), config: Data::new(RwLock::new(config)), }; - if update_config_periodically { - client.clone().start_polling_update().await; - } Ok(client) } @@ -139,30 +142,16 @@ impl Client { Ok(format!("{}: CAC updated successfully", self.tenant)) } - pub async fn start_polling_update(self) { - rt::spawn(async move { - let mut interval = interval(self.polling_interval); - loop { - interval.tick().await; - let result = self.update_cac().await.unwrap_or_else(identity); - log::info!("{result}",); - } - }); - } - - pub async fn ffi_polling_update(self) { - println!("into polling updates"); + pub async fn run_polling_updates(self: Arc<Self>) { let mut interval = interval(self.polling_interval); loop { - println!("started polling updates"); interval.tick().await; let result = self.update_cac().await.unwrap_or_else(identity); - println!("{result}"); log::info!("{result}",); } } - pub fn get_config(&self) -> Result<Config, String> { + pub fn get_full_config_state(&self) -> Result<Config, String> { self.config.read().map(|c| c.clone()).map_err_to_string() } @@ -184,6 +173,46 @@ impl Client { merge_strategy, ) } + + pub fn get_resolved_config( + &self, + query_data: Map<String, Value>, + keys: Vec<String>, + merge_strategy: MergeStrategy, + ) -> Result<Map<String, Value>, String> { + let cac = self.eval(query_data, merge_strategy)?; + if keys.is_empty() { + return Ok(cac); + } + Ok(Map::from_iter( + cac.iter() + .filter_map(|(config, v)| { + if keys.contains(config) { + Some((config.to_owned(), v.to_owned())) + } else { + None + } + }) + .collect::<Vec<(String, Value)>>(), + )) + } + + pub fn get_default_config( + &self, + keys: Vec<String>, + ) -> Result<Map<String, Value>, String> { + let configs = self.config.read().map_err(|e| e.to_string())?; + let default_configs = configs.default_configs.clone(); + let default_configs = if keys.len() > 0 { + default_configs + .into_iter() + .filter(|(item, _)| keys.contains(item)) + .collect::<Map<String, Value>>() + } else { + default_configs + }; + Ok(default_configs) + } } #[derive(Deref, DerefMut)] @@ -192,7 +221,6 @@ impl ClientFactory { pub async fn create_client( &self, tenant: String, - update_config_periodically: bool, polling_interval: Duration, hostname: String, ) -> Result<Arc<Client>, String> { @@ -208,15 +236,8 @@ impl ClientFactory { return Ok(client.clone()); } - let client = Arc::new( - Client::new( - tenant.to_string(), - update_config_periodically, - polling_interval, - hostname, - ) - .await?, - ); + let client = + Arc::new(Client::new(tenant.to_string(), polling_interval, hostname).await?); factory.insert(tenant.to_string(), client.clone()); return Ok(client.clone()); } diff --git a/crates/experimentation_client/CHANGELOG.md b/crates/experimentation_client/CHANGELOG.md index ba9eb311e..9d21a2949 100644 --- a/crates/experimentation_client/CHANGELOG.md +++ b/crates/experimentation_client/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - -## superposition_client-v0.5.0 - 2024-03-18 +## experimentation_client-v0.5.0 - 2024-03-18 #### Documentation - PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan #### Features diff --git a/crates/experimentation_client/Cargo.toml b/crates/experimentation_client/Cargo.toml index 538fa5b29..4e0e8f819 100644 --- a/crates/experimentation_client/Cargo.toml +++ b/crates/experimentation_client/Cargo.toml @@ -16,7 +16,7 @@ derive_more = { workspace = true } log = { workspace = true } [lib] -name = "superposition_client" +name = "experimentation_client" crate-type = ["cdylib", "lib"] [build-dependencies] diff --git a/crates/experimentation_client/build.rs b/crates/experimentation_client/build.rs index 707b90a57..2530a7a98 100644 --- a/crates/experimentation_client/build.rs +++ b/crates/experimentation_client/build.rs @@ -6,5 +6,5 @@ fn main() { config.language = cbindgen::Language::C; cbindgen::generate_with_config(&crate_dir, config) .unwrap() - .write_to_file("../../headers/libsuperposition_client.h"); + .write_to_file("../../headers/libexperimentation_client.h"); } diff --git a/crates/experimentation_client/src/lib.rs b/crates/experimentation_client/src/lib.rs index dbd3ffd93..a71143021 100644 --- a/crates/experimentation_client/src/lib.rs +++ b/crates/experimentation_client/src/lib.rs @@ -1,7 +1,5 @@ mod interface; mod types; -mod interface; -mod types; use std::{collections::HashMap, sync::Arc}; use chrono::{DateTime, TimeZone, Utc}; diff --git a/crates/experimentation_client_integration_example/src/main.rs b/crates/experimentation_client_integration_example/src/main.rs index ebc4e5247..f071a3ed0 100644 --- a/crates/experimentation_client_integration_example/src/main.rs +++ b/crates/experimentation_client_integration_example/src/main.rs @@ -4,8 +4,8 @@ use actix_web::{ App, HttpResponse, HttpServer, }; -use serde_json::json; use experimentation_client as exp; +use serde_json::json; #[actix_web::main] async fn main() -> std::io::Result<()> { diff --git a/docs/client-context-aware-configuration.md b/docs/client-context-aware-configuration.md index e67fc4ade..eae44efe4 100644 --- a/docs/client-context-aware-configuration.md +++ b/docs/client-context-aware-configuration.md @@ -1,19 +1,104 @@ -# Context Aware Configuration Client Integration +# Context Aware Config Client Integration +---- -This provides SDK to interact with ```context-aware-config```.We support cac_client for - 1. Rust - 2. Haskell +This provides SDK to interact with ```context-aware-config```. +- [Context Aware Config Client Integration](#context-aware-config-client-integration) + - [Rust](#rust) + - [Client Factory Methods Reference](#client-factory-methods-reference) + - [Create Client](#create-client) + - [Function definition](#function-definition) + - [Params](#params) + - [Get Client](#get-client) + - [Function definition](#function-definition-1) + - [Params](#params-1) + - [Example Implementation](#example-implementation) + - [CAC Client Methods Reference](#cac-client-methods-reference) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service) + - [Function definition](#function-definition-2) + - [Get Config](#get-config) + - [Funtion Definition](#funtion-definition) + - [Get the last modified Time](#get-the-last-modified-time) + - [Function Definition](#function-definition-3) + - [Evaluate Context to derive configs](#evaluate-context-to-derive-configs) + - [Function Definition](#function-definition-4) + - [Params](#params-2) + - [Get Default Config](#get-default-config) + - [Function Definition](#function-definition-5) + - [Param](#param) + - [Haskell](#haskell) + - [Adding the clients to your project](#adding-the-clients-to-your-project) + - [Nix](#nix) + - [Haskell CAC client functions reference](#haskell-cac-client-functions-reference) + - [Create a client](#create-a-client) + - [Function Definition](#function-definition-6) + - [Param](#param-1) + - [Get a client](#get-a-client) + - [Function Definition](#function-definition-7) + - [Param](#param-2) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service-1) + - [Function definition](#function-definition-8) + - [Param](#param-3) + - [Get Config](#get-config-1) + - [Funtion Definition](#funtion-definition-1) + - [Get the last modified Time](#get-the-last-modified-time-1) + - [Function Definition](#function-definition-9) + - [Evaluate Context to derive configs](#evaluate-context-to-derive-configs-1) + - [Function Definition](#function-definition-10) + - [Params](#params-3) + - [Get Default Config](#get-default-config-1) + - [Function Definition](#function-definition-11) + - [Sample Integration](#sample-integration) +--- ## Rust -### Implementation -Below is the rust implementation to instantiate CAC client . +The rust client have a client factory that helps you work with multiple clients connected to different tenants + +### Client Factory Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client, it is not mandatory to consume the `Ok` value. + +##### Function definition +``` +pub async fn create_client( + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `polling_interval` | Duration | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +pub async fn get_client( + tenant: String + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Example Implementation + +Below is the rust implementation to instantiate CAC client using the client factory. ```rust use cac_client as cc; -let tenants: Vec<String> = ["abc", "wer"]; +let tenants: Vec<String> = ["dev", "test"]; //You can create a clientFactory for tenant in tenants { cc::CLIENT_FACTORY @@ -21,30 +106,267 @@ for tenant in tenants { tenant.to_string(), update_cac_periodically,//flag for if you want to update cac config periodically polling_interval,//polling interval in secs, default is 60 - cac_hostname.to_string(),//cac server host + cac_hostname.to_string(),// superposition service host ) .await .expect(format!("{}: Failed to acquire cac_client", tenant).as_str()); }; //You can extract an individual tenant's client from clientFactory -let tenant = "abc".to_owned(); +let tenant = "dev".to_owned(); let cac_client = cc::CLIENT_FACTORY.get_client(tenant.clone()).map_err(|e| { log::error!("{}: {}", tenant.clone(), e); ErrorType::IgnoreError(format!("{}: Failed to get cac client", tenant)) })?; +``` + +### CAC Client Methods Reference + +After calling `get_client` method of Client Factory, you can do the following with the `Client` returned. + +#### Run polling for updates from Superposition Service + +the CAC client polls for updates from the superposition service and loads any changes done on the server. This means that configs changed in superposition are reflected on the client in the duration of `polling_interval`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + pub async fn run_polling_updates() + ``` + +#### Get Config + +Get the full config definition of your tenants configuration from superposition. `Config` has the following information: + +``` +pub struct Config { + contexts: Vec<Context>, + overrides: Map<String, Value>, + default_configs: Map<String, Value>, +} +``` + +##### Funtion Definition + +``` +pub fn get_full_config_state() -> Result<Config, String> +``` + +#### Get the last modified Time + +CAC client lets you get the last modified time of your configs, in case you want to log it, etc. + +##### Function Definition + +``` +pub fn get_last_modified() -> Result<DateTime<Utc>, String> +``` + +#### Evaluate Context to derive configs +Given a context, get overrides for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. + +##### Function Definition ``` +pub fn get_resolved_config(context: Map<String, Value>, keys: Vec<String>) -> Result<Map<String, Value>, String> +``` +##### Params + +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | Map<String, Value> | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `keys` | Vec<String> | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | -### Methods Overview +#### Get Default Config -1. ```pub async fn start_polling_update(self)``` -> This updates client's config at given interval. -1. ```pub fn get_config(&self) -> Result<Config, String> {}``` -> You can fetch the client's config. -2. ```pub fn get_last_modified<E>(&'static self) -> Result<DateTime<Utc>, String> {}``` -> This provides when the config last modified. -3. ```pub fn eval(&self, query_data: Map<String, Value>,) -> Result<Map<String, Value>, String> {} ``` --> This helps in evaluating the config based on the given context. +The default config for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. +##### Function Definition + +``` +pub fn get_default_config(keys: Vec<String>) -> Result<Map<String, Value>, String> +``` +##### Param +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `keys` | Vec<String> | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | + +--- ## Haskell +### Adding the clients to your project + +#### Nix + +Add the following to your inputs + +``` +crane.url = "github:ipetkov/crane/54b63c8eae4c50172cb50b612946ff1d2bc1c75c"; +crane.inputs.nixpkgs.follows = "common/nixpkgs"; +context-aware-config = { + url = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config"; + inputs.nixpkgs.follows = "common/nixpkgs"; + inputs.crane.follows = "crane"; +}; +``` + +then, add the following to your imports section in outputs: + +``` +imports = [ + ...... + inputs.context-aware-config.haskellFlakeProjectModules.output +] +``` + +then add the libraries to your project.cabal file: + +``` +extra-libraries: + cac_client + experimentation_client +``` +### Haskell CAC client functions reference + +#### Create a client + +Create a new client in the Client Factory + +##### Function Definition + +``` +createCacClient:: Tenant -> Interval -> Hostname -> IO (Either Error ()) +``` + +##### Param + +| Param | type | description | Example value | +| ---------- | -------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | +| `Interval` | Duration | specifies the time cac client waits before checking with the server for updates, in seconds | 10 | +| `Hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get a client + +Create a new client in the Client Factory + +##### Function Definition + +``` +getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) +``` + +##### Param + +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------------------------------------------------------------------ | ------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | + + +#### Run polling for updates from Superposition Service + +the CAC client polls for updates from the superposition service and loads any changes done on the server. This means that configs changed in superposition are reflected on the client in the duration of `Interval`. `cacStartPolling` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` +cacStartPolling :: Tenant -> IO () + ``` + +##### Param + +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------------------------------------------------------------------ | ------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | + +#### Get Config + +Get the full config definition of your tenants configuration from superposition. `Config` has the following information: + +``` +{ + contexts: [Context], + overrides: Map String Value, + default_configs: Map String Value, +} +``` + +##### Funtion Definition + +``` +getFullConfigState :: ForeignPtr CacClient -> IO (Either Error Value) +``` + +#### Get the last modified Time + +CAC client lets you get the last modified time of your configs, in case you want to log it, etc. + +##### Function Definition + +``` +getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) +``` + +#### Evaluate Context to derive configs + +Given a context, get overrides for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. + +##### Function Definition + +``` +getResolvedConfig :: ForeignPtr CacClient -> String -> [String] -> IO (Either Error Value) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `keys` | [String] | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | + +#### Get Default Config + +The default config for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. + +##### Function Definition + +``` +getDefaultConfig :: ForeignPtr CacClient -> [String] -> IO (Either Error Value) +``` +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `keys` | [String] | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | + +#### Sample Integration + +``` +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (getResolvedConfig, createCacClient, getCacClient, + getFullConfigState, getCacLastModified, cacStartPolling, getDefaultConfig) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createCacClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkOS (cacStartPolling "dev") + print threadId + getCacClient "dev" >>= \case + Left err -> putStrLn err + Right client -> do + config <- getFullConfigState client + lastModified <- getCacLastModified client + overrides <- getResolvedConfig client "{\"country\": \"India\"}" ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client ["country_image_url", "hyperpay_version"] + print config + print lastModified + print overrides + print defaults + threadDelay 1000000000 + pure () +``` \ No newline at end of file diff --git a/docs/client-experimentation.md b/docs/client-experimentation.md index fadcc07a1..686fede54 100644 --- a/docs/client-experimentation.md +++ b/docs/client-experimentation.md @@ -1,19 +1,97 @@ # Experimentation Client Integration -This provides SDK to interact with ```experimentation-platform```.We support experimentation_client for - 1. Rust - 2. Haskell +This provides SDK to interact with ```experimentation-platform``` + +- [Experimentation Client Integration](#experimentation-client-integration) + - [Rust](#rust) + - [Client Factory Methods Reference](#client-factory-methods-reference) + - [Create Client](#create-client) + - [Function definition](#function-definition) + - [Params](#params) + - [Get Client](#get-client) + - [Function definition](#function-definition-1) + - [Params](#params-1) + - [Example Implementation](#example-implementation) + - [Experiment Client Methods Reference](#experiment-client-methods-reference) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service) + - [Function definition](#function-definition-2) + - [Get an applicable variant](#get-an-applicable-variant) + - [Function Definition](#function-definition-3) + - [Params](#params-2) + - [Get satisfied experiments](#get-satisfied-experiments) + - [Function Definition](#function-definition-4) + - [Params](#params-3) + - [Get all running experiments](#get-all-running-experiments) + - [Function Definition](#function-definition-5) + - [Haskell](#haskell) + - [Experiment Client Methods Reference](#experiment-client-methods-reference-1) + - [Create Client](#create-client-1) + - [Function definition](#function-definition-6) + - [Params](#params-4) + - [Get Client](#get-client-1) + - [Function definition](#function-definition-7) + - [Params](#params-5) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service-1) + - [Function definition](#function-definition-8) + - [Get an applicable variant](#get-an-applicable-variant-1) + - [Function Definition](#function-definition-9) + - [Params](#params-6) + - [Get satisfied experiments](#get-satisfied-experiments-1) + - [Function Definition](#function-definition-10) + - [Params](#params-7) + - [Get all running experiments](#get-all-running-experiments-1) + - [Function Definition](#function-definition-11) + - [Sample Integration](#sample-integration) ## Rust -### Implementation +The rust client have a client factory that helps you work with multiple clients connected to different tenants + +### Client Factory Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client, it is not mandatory to consume the `Ok` value. + +##### Function definition +``` +pub async fn create_client( + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `polling_interval` | Duration | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +pub async fn get_client( + tenant: String + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Example Implementation + Below is the rust implementation to instantiate Experimentation client . ```rust use superpostion_client as sp; -let tenants: Vec<String> = ["abc", "wer"]; +let tenants: Vec<String> = ["dev", "test"]; //You can create a clientFactory for tenant in tenants { rt::spawn( @@ -29,7 +107,7 @@ for tenant in tenants { ); }; //You can extract an individual tenant's client from clientFactory -let tenant = "abc".to_owned(); +let tenant = "dev".to_owned(); let sp_client = sp::CLIENT_FACTORY .get_client(tenant.clone()) .await @@ -41,12 +119,176 @@ let sp_client = sp::CLIENT_FACTORY ``` -### Methods Overview +### Experiment Client Methods Reference + +#### Run polling for updates from Superposition Service + +the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client in the duration of `polling_interval`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + pub async fn run_polling_updates() + ``` + +#### Get an applicable variant + +When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. + +The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant + +##### Function Definition +``` +pub async fn get_applicable_variant(context: &Value, toss: i8) -> Vec<String> +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `toss` | i8 | an integer that assigns your request to a variant | `4` | + +#### Get satisfied experiments + +Rather than just getting the variant ID, you can get the whole experiment(s) that are satisfying your context - rather than just the final result. + +##### Function Definition +``` +pub async fn get_satisfied_experiments(context: &Value) -> Experiments +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | + +#### Get all running experiments + +Get all running experiments, why would you want to do this? We don't know. But you can. + +##### Function Definition +``` +pub async fn get_running_experiments() -> Experiments +``` + +## Haskell + +### Experiment Client Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client. + +##### Function definition +``` +createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `Interval` | Integer | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `Hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Run polling for updates from Superposition Service + +the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client in the duration of `Interval`. `expStartPolling` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + expStartPolling :: Tenant -> IO () + ``` + +#### Get an applicable variant + +When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. + +The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant + +##### Function Definition +``` +getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `toss` | Integer | an integer that assigns your request to a variant | `4` | + +#### Get satisfied experiments + +Rather than just getting the variant ID, you can get the whole experiment(s) that are satisfying your context - rather than just the final result. + +##### Function Definition +``` +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | + +#### Get all running experiments + +Get all running experiments, why would you want to do this? We don't know. But you can. + +##### Function Definition +``` +getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) +``` + +#### Sample Integration + +``` +{-# LANGUAGE LambdaCase #-} +module Main (main) where -1. ```pub async fn run_polling_updates(self: Arc<Self>) {}``` -> You can set the interval for updating config in the client. -2. ```pub async fn get_applicable_variant(&self, context: &Value, toss: i8) -> Vec<String> {}``` -> Provides variantIds for which the given context holds true. -3. ```pub async fn get_satisfied_experiments(&self, context: &Value) -> Experiments {}``` -> Lists all the experiments for the given context. -4. ``` pub async fn get_running_experiments(&self) -> Experiments {}``` -> This lists only the inprogress experiments. +import Client (createExpClient, expStartPolling, + getApplicableVariants, getExpClient, + getRunningExperiments, + getSatisfiedExperiments) +import Control.Concurrent +import Prelude +main :: IO () +main = do + createExpClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkIO (expStartPolling "dev") + print threadId + getExpClient "dev" >>= \case + Left err -> putStrLn err + Right client -> loop client + pure () + where + loop client = do + runningExperiments <- getRunningExperiments client + satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" + variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + print "Running experiments" + print runningExperiments + print "experiments that satisfy context" + print satisfiedExperiments + print "variant ID applied" + print variants + -- threadDelay 10000000 + loop client -## Haskell \ No newline at end of file +``` \ No newline at end of file diff --git a/flake.nix b/flake.nix index fa16e1000..33852394a 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,7 @@ devShells.default = pkgs.mkShell { inputsFrom = [ self'.devShells.rust + self'.devShells.haskell ]; packages = with pkgs; [ docker-compose @@ -42,4 +43,4 @@ }; }; }; -} \ No newline at end of file +} diff --git a/headers/libcac_client.h b/headers/libcac_client.h index 07141a7a9..0b9eab167 100644 --- a/headers/libcac_client.h +++ b/headers/libcac_client.h @@ -11,10 +11,7 @@ const char *last_error_message(void); void free_string(char *s); -int new_client(const char *tenant, - bool update_periodically, - unsigned long update_frequency, - const char *hostname); +int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); void start_polling_update(const char *tenant); @@ -22,8 +19,13 @@ void free_client(struct Arc_Client *ptr); struct Arc_Client *get_client(const char *tenant); -const char *cac_eval(struct Arc_Client *client, const char *query, const char *merge_strategy); - const char *get_last_modified(struct Arc_Client *client); const char *get_config(struct Arc_Client *client); + +const char *get_resolved_config(struct Arc_Client *client, + const char *query, + const char *keys, + const char *merge_strategy); + +const char *get_default_config(struct Arc_Client *client, const char *keys); diff --git a/headers/libsuperposition_client.h b/headers/libexperimentation_client.h similarity index 54% rename from headers/libsuperposition_client.h rename to headers/libexperimentation_client.h index a3c1a629d..f37cbf580 100644 --- a/headers/libsuperposition_client.h +++ b/headers/libexperimentation_client.h @@ -24,23 +24,3 @@ char *get_applicable_variant(struct Arc_Client *client, const char *c_context, s char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); char *get_running_experiments(struct Arc_Client *client); - -int last_error_length(void); - -const char *last_error_message(void); - -void free_string(char *s); - -int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); - -void start_polling_update(const char *tenant); - -void free_client(struct Arc_Client *ptr); - -struct Arc_Client *get_client(const char *tenant); - -char *get_applicable_variant(struct Arc_Client *client, const char *c_context, short toss); - -char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); - -char *get_running_experiments(struct Arc_Client *client); From 7e859fc6aadd946a579b000b70dab21e42cc5d2a Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Tue, 12 Mar 2024 19:27:12 +0530 Subject: [PATCH 326/352] feat: function ui --- crates/frontend/src/api.rs | 44 +- crates/frontend/src/app.rs | 40 ++ .../src/components/experiment/experiment.rs | 22 +- .../components/function_form/function_form.rs | 291 ++++++++++ .../src/components/function_form/mod.rs | 3 + .../src/components/function_form/types.rs | 16 + .../src/components/function_form/utils.rs | 107 ++++ crates/frontend/src/components/mod.rs | 1 + .../src/components/side_nav/side_nav.rs | 6 + .../frontend/src/pages/function/function.rs | 505 ++++++++++++++++++ .../src/pages/function/function_create.rs | 61 +++ .../src/pages/function/function_list.rs | 175 ++++++ crates/frontend/src/pages/function/mod.rs | 4 + crates/frontend/src/pages/function/utils.rs | 57 ++ crates/frontend/src/pages/mod.rs | 1 + crates/frontend/src/types.rs | 18 +- 16 files changed, 1336 insertions(+), 15 deletions(-) create mode 100644 crates/frontend/src/components/function_form/function_form.rs create mode 100644 crates/frontend/src/components/function_form/mod.rs create mode 100644 crates/frontend/src/components/function_form/types.rs create mode 100644 crates/frontend/src/components/function_form/utils.rs create mode 100644 crates/frontend/src/pages/function/function.rs create mode 100644 crates/frontend/src/pages/function/function_create.rs create mode 100644 crates/frontend/src/pages/function/function_list.rs create mode 100644 crates/frontend/src/pages/function/mod.rs create mode 100644 crates/frontend/src/pages/function/utils.rs diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 642d5c701..a8f3db2be 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -2,7 +2,8 @@ use leptos::ServerFnError; use crate::{ types::{ - Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, ListFilters, + Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, + FunctionResponse, ListFilters, }, utils::use_host_server, }; @@ -87,6 +88,47 @@ pub async fn fetch_experiments( Ok(response) } +pub async fn fetch_functions( + tenant: String, +) -> Result<Vec<FunctionResponse>, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/function", host); + let response: Vec<FunctionResponse> = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +pub async fn fetch_function( + function_name: String, + tenant: String, +) -> Result<FunctionResponse, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/function/{}", host, function_name); + let response: FunctionResponse = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + // #[server(GetConfig, "/fxn", "GetJson")] pub async fn fetch_config(tenant: String) -> Result<Config, ServerFnError> { let client = reqwest::Client::new(); diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index c10dae503..398391e3f 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -5,6 +5,10 @@ use serde_json::json; use crate::hoc::layout::layout::Layout; use crate::pages::experiment_list::experiment_list::ExperimentList; +use crate::pages::function::{ + function::FunctionPage, function_create::CreateFunctionView, + function_list::FunctionList, +}; use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::{ ContextOverride::context_override::ContextOverride, @@ -96,6 +100,42 @@ pub fn App(app_envs: Envs) -> impl IntoView { } /> + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function" + view=move || { + view! { + <Layout> + <FunctionList/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function/create" + view=move || { + view! { + <Layout> + <CreateFunctionView/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function/:function_name" + view=move || { + view! { + <Layout> + <FunctionPage/> + </Layout> + } + } + /> + <Route ssr=SsrMode::Async path="/admin/:tenant/experiments" diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index 34f90c61e..dab217785 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -158,20 +158,16 @@ where {move || { let mut view = Vec::new(); for token in contexts.clone() { - let (dimension, value) = (token.left_operand, token.right_operand); - view.push( - view! { - <div class="stat w-3/12"> - <div class="stat-title"> - {dimension} - </div> - <div class="stat-value text-base"> - {&value.replace("\"", "")} - - </div> + let (dimension, value) = (token.left_operand, token.right_operand); + view.push( + view! { + <div class="stat w-3/12"> + <div class="stat-title">{dimension}</div> + <div class="stat-value text-base">{&value.replace("\"", "")} </div> - }, - ); + </div> + }, + ); } view }} diff --git a/crates/frontend/src/components/function_form/function_form.rs b/crates/frontend/src/components/function_form/function_form.rs new file mode 100644 index 000000000..4da400b00 --- /dev/null +++ b/crates/frontend/src/components/function_form/function_form.rs @@ -0,0 +1,291 @@ +use super::utils::{create_function, test_function, update_function}; +use crate::components::button::button::Button; +use leptos::*; +use serde_json::{from_str, json, Value}; +use web_sys::MouseEvent; + +#[component] +pub fn function_editor<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] function_name: String, + #[prop(default = String::new())] function: String, + #[prop(default = String::new())] runtime_version: String, + #[prop(default = String::new())] description: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (function_name, set_function_name) = create_signal(function_name); + let (function, set_function) = create_signal(function); + let (runtime_version, set_runtime_version) = create_signal(runtime_version); + let (error_message, set_error_message) = create_signal("".to_string()); + let (description, set_description) = create_signal(description); + if !edit { + set_runtime_version.set("1.0.0".to_string()) + }; + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + + let tenant = tenant_rs.get(); + let f_function_name = function_name.get(); + let f_function = function.get(); + let f_runtime_version = runtime_version.get(); + let f_description = description.get(); + let handle_submit_clone = handle_submit.clone(); + + logging::log!("Function Name in editor: {:?}", function_name); + + spawn_local({ + async move { + let result = if edit == true { + update_function( + f_function_name, + f_function, + f_runtime_version, + f_description, + tenant, + ) + .await + } else { + create_function( + f_function_name, + f_function, + f_runtime_version, + f_description, + tenant, + ) + .await + }; + + match result { + Ok(_) => { + handle_submit_clone(); + } + Err(e) => { + set_error_message.set(e); + } + } + } + }); + }; + + view! { + <div> + <form id="MyForm"> + + <div class="flex flex-row w-full justify-between"> + <div class="form-group"> + + <div + class="monaco" + id="function" + style="min-height: 500px; min-width: 1000px" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("Function editor - Function Name: {:?}", value); + set_function.set(value); + } + ></div> + </div> + + <div class="mx-auto w-auto" style="width: 250px"> + + <Show when=move || { edit == false }> + <div class="form-control "> + <label class="label"> + <span class="label-text">Function Name</span> + </label> + <input + disabled=edit + value=move || function_name.get() + on:input=move |ev| { + set_function_name.set(event_target_value(&ev)) + } + type="text" + name="funName" + id="funName" + placeholder="ex: myfunction" + class="input input-bordered w-full max-w-md" + /> + </div> + </Show> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Draft Runtime Version</span> + </label> + <input + disabled=true + value=move || runtime_version.get() + on:input=move |ev| set_runtime_version.set(event_target_value(&ev)) + type="text" + name="runVersion" + id="runVersion" + placeholder="Js Runtime Version" + class="input input-bordered " + /> + </div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Description</span> + </label> + <textarea + type="text" + class="input input-bordered shadow-md" + name="description" + id="description" + placeholder="explain function" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_description.set(value); + } + > + + {description.get()} + </textarea> + </div> + + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + <div class="flex"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + </div> + + </div> + </form> + </div> + } +} + +#[component] +pub fn test_form<NF>( + function_name: String, + stage: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (error_message, set_error_message) = create_signal("".to_string()); + let (output_message, set_output_message) = create_signal("".to_string()); + let (val, set_val) = create_signal(json!({})); + let (key, set_key) = create_signal("".to_string()); + + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + + let tenant = tenant_rs.get(); + let f_function_name = function_name.clone(); + let f_val = json!({ + "key": key.get(), + "value": val.get() + }); + let f_stage = stage.clone(); + + logging::log!("{:?}", function_name); + logging::log!("{:?}", val); + + spawn_local({ + async move { + let result = test_function(f_function_name, f_stage, f_val, tenant).await; + + match result { + Ok(resp) => { + set_error_message.set("".to_string()); + set_output_message.set(resp); + } + Err(e) => { + set_output_message.set("".to_string()); + set_error_message.set(e); + } + } + } + }); + }; + + view! { + <div class="flex flex-row justify-between"> + + <div class="monaco" style="min-height: 500px; min-width: 1000px"></div> + + <div class="mx-auto w-auto" style="width: 250px"> + + <form id="MyForm"> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Key Name</span> + </label> + <input + disabled=false + value=move || key.get() + on:input=move |ev| set_key.set(event_target_value(&ev)) + type="text" + name="key name" + id="keyName" + placeholder="key" + class="input input-bordered" + /> + </div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Value</span> + </label> + <textarea + type="text" + class="input input-bordered shadow-md" + name="value" + id="value" + style="min-height: 150px" + placeholder="value" + on:change=move |ev| { + let value = event_target_value(&ev); + match from_str::<Value>(&value) { + Ok(test_val) => { + set_val.set(test_val); + set_error_message.set("".to_string()); + set_output_message.set("".to_string()); + } + Err(e) => { + set_val.set(json!(value)); + set_error_message.set("".to_string()); + set_output_message.set("".to_string()); + + } + }; + } + > + + {"".to_string()} + </textarea> + + </div> + + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + <div class="mt-7"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + + <div> + <p class="text-red-500">{move || output_message.get()}</p> + </div> + + </form> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/function_form/mod.rs b/crates/frontend/src/components/function_form/mod.rs new file mode 100644 index 000000000..fb05a0779 --- /dev/null +++ b/crates/frontend/src/components/function_form/mod.rs @@ -0,0 +1,3 @@ +pub mod function_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/function_form/types.rs b/crates/frontend/src/components/function_form/types.rs new file mode 100644 index 000000000..f4fe58975 --- /dev/null +++ b/crates/frontend/src/components/function_form/types.rs @@ -0,0 +1,16 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct FunctionCreateRequest { + pub function_name: String, + pub function: String, + pub runtime_version: String, + pub description: String, +} + +#[derive(Serialize)] +pub struct FunctionUpdateRequest { + pub function: String, + pub runtime_version: String, + pub description: String, +} diff --git a/crates/frontend/src/components/function_form/utils.rs b/crates/frontend/src/components/function_form/utils.rs new file mode 100644 index 000000000..5e006cc04 --- /dev/null +++ b/crates/frontend/src/components/function_form/utils.rs @@ -0,0 +1,107 @@ +use super::types::{FunctionCreateRequest, FunctionUpdateRequest}; +use crate::utils::get_host; +use reqwest::StatusCode; +use serde_json::{json, Value}; + +pub async fn create_function( + function_name: String, + function: String, + runtime_version: String, + description: String, + tenant: String, +) -> Result<String, String> { + let payload = FunctionCreateRequest { + function_name, + function, + runtime_version, + description, + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function"); + let request_payload = json!(payload); + let response = client + .post(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} + +pub async fn update_function( + function_name: String, + function: String, + runtime_version: String, + description: String, + tenant: String, +) -> Result<String, String> { + let payload = FunctionUpdateRequest { + function, + runtime_version, + description, + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}"); + let request_payload = json!(payload); + + let response = client + .patch(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} + +pub async fn test_function( + function_name: String, + stage: String, + val: Value, + tenant: String, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}/{stage}/test"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .json(&val) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs index fe02ca5bb..55c922460 100644 --- a/crates/frontend/src/components/mod.rs +++ b/crates/frontend/src/components/mod.rs @@ -9,6 +9,7 @@ pub mod experiment; pub mod experiment_conclude_form; pub mod experiment_form; pub mod experiment_ramp_form; +pub mod function_form; pub mod modal; pub mod nav_item; pub mod override_form; diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs index d920ce2ae..42cc051b4 100644 --- a/crates/frontend/src/components/side_nav/side_nav.rs +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -15,6 +15,12 @@ fn create_routes(tenant: &str) -> Vec<AppRoute> { icon: "ri-test-tube-fill".to_string(), label: "Experiments".to_string(), }, + AppRoute { + key: format!("{base}/admin/{tenant}/function"), + path: format!("{base}/admin/{tenant}/function"), + icon: "ri-code-box-fill".to_string(), + label: "Functions".to_string(), + }, AppRoute { key: format!("{base}/admin/{tenant}/dimensions"), path: format!("{base}/admin/{tenant}/dimensions"), diff --git a/crates/frontend/src/pages/function/function.rs b/crates/frontend/src/pages/function/function.rs new file mode 100644 index 000000000..d1be9eec8 --- /dev/null +++ b/crates/frontend/src/pages/function/function.rs @@ -0,0 +1,505 @@ +use leptos::*; +use leptos_router::use_params_map; + +use crate::types::FunctionResponse; + +use super::utils::publish_function; + +use crate::api::fetch_function; +use std::time::Duration; +use strum::EnumProperty; +use strum_macros::Display; +use web_sys::MouseEvent; + +use crate::utils::get_element_by_id; +use web_sys::HtmlButtonElement; + +use crate::components::function_form::function_form::{FunctionEditor, TestForm}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Copy, Display, strum_macros::EnumProperty, PartialEq)] +enum CodeTab { + #[strum(props(id = "draft_code_tab"))] + DraftCode, + #[strum(props(id = "published_code_tab"))] + PublishedCode, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + function: Option<FunctionResponse>, +} + +#[component] +pub fn function_page() -> impl IntoView { + let function_params = use_params_map(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let source = move || { + let t = tenant_rs.get(); + let function_name = function_params + .with(|params| params.get("function_name").cloned().unwrap_or("1".into())); + (function_name, t) + }; + + let (selected_tab_rs, selected_tab_ws) = create_signal(CodeTab::PublishedCode); + let (editor_mode_rs, editor_mode_ws) = create_signal(true); + let (test_mode_rs, test_mode_ws) = create_signal(false); + let (show_publish_rs, show_publish_ws) = create_signal(false); + let (publish_error_rs, publish_error_ws) = create_signal("".to_string()); + + let combined_resource: Resource<(String, String), CombinedResource> = + create_blocking_resource(source, |(function_name, tenant)| async move { + let function_result = + fetch_function(function_name.to_string(), tenant.to_string()).await; + + CombinedResource { + function: function_result.ok(), + } + }); + + view! { + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> + {move || { + let resource = match combined_resource.get() { + Some(res) => res, + None => return view! { <h1>Error fetching function</h1> }.into_view(), + }; + let function = resource.function; + match function { + Some(function) => { + let function_ef = function.clone(); + let function_data = function.clone(); + publish_error_ws.set("".to_string()); + match function.published_at.clone() { + Some(val) => show_publish_ws.set(val < function.draft_edited_at), + None => show_publish_ws.set(true), + } + let publish_click = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + let tenant = tenant_rs.get(); + let f_function_name = function_ef.function_name.clone(); + spawn_local({ + async move { + let result = publish_function(f_function_name, tenant).await; + match result { + Ok(_) => { + publish_error_ws.set("".to_string()); + combined_resource.refetch(); + } + Err(e) => { + publish_error_ws.set(e); + } + } + } + }); + }; + view! { + + <div class="flex flex-col flex-row overflow-x-auto p-2 bg-transparent"> + + <div class="flex bg-base-100 flex-row gap-3 justify-between flex-wrap shadow m-5"> + <div class="stat w-2/12"> + <div class="stat-title">Function Name</div> + <div>{function.function_name.clone()}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Published Runtime Version</div> + <div> + {function + .published_runtime_version + .clone() + .unwrap_or("null".to_string())} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Function Description</div> + <div> + {format!("{}", function.function_description.clone())} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Draft Edited At</div> + <div> + + {format!( + "{}", + function.draft_edited_at.clone().format("%v").to_string(), + )} + + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Published At</div> + <div> + + {match function.published_at.clone() { + Some(val) => val.format("%v").to_string(), + None => "null".to_string(), + }} + + </div> + </div> + + </div> + + <div class="flex flex-row justify-between"> + <div + role="tablist" + class="flex-row tabs m-6 w-30 self-start tabs-lifted tabs-md" + > + <a + role="tab" + id=CodeTab::PublishedCode + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + CodeTab::PublishedCode => { + "tab tab-active [--tab-border-color:#a651f5] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(CodeTab::PublishedCode); + publish_error_ws.set("".to_string()); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + Published Code + </a> + <a + role="tab" + id=CodeTab::DraftCode + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + CodeTab::DraftCode => { + "tab tab-active [--tab-border-color:orange] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(CodeTab::DraftCode); + publish_error_ws.set("".to_string()); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + Draft Code + </a> + </div> + <div> + {move || { + selected_tab_rs + .with(|tab| { + match tab { + CodeTab::PublishedCode => { + view! { + <div> + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { test_mode_ws.set(false) } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + + </Show> + + <Show when=move || { test_mode_rs.get() == false }> + <div class="flex flex-row justify-end join m-5"> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { test_mode_ws.set(true) } + > + <i class="fontisto-test-tube-alt"></i> + Test + </button> + </div> + + </Show> + </div> + } + } + CodeTab::DraftCode => { + let publish_click_ef = publish_click.clone(); + view! { + <div> + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + test_mode_ws.set(false); + } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + </Show> + + <Show when=move || { editor_mode_rs.get() == false }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + editor_mode_ws.set(true); + } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + + </Show> + <div class="flex flex-row justify-end join m-5"> + <Show when=move || { + editor_mode_rs.get() == true && test_mode_rs.get() == false + && show_publish_rs.get() + }> + <div class="flex"> + <p class="text-red-500">{move || publish_error_rs.get()}</p> + </div> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=publish_click_ef.clone() + > + <i class="fontisto-test-tube-alt"></i> + Publish + </button> + + </Show> + <Show when=move || { + editor_mode_rs.get() == true && test_mode_rs.get() == false + }> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + editor_mode_ws.set(false); + publish_error_ws.set("".to_string()) + } + > + <i class="ri-edit-line"></i> + Edit + + </button> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + test_mode_ws.set(true); + publish_error_ws.set("".to_string()) + } + > + <i class="fontisto-test-tube-alt"></i> + Test + </button> + + </Show> + </div> + </div> + } + } + } + }) + }} + + </div> + + </div> + + {move || { + let is_edit = editor_mode_rs.get(); + let is_test = test_mode_rs.get(); + let should_show = (editor_mode_rs.get() == true) + && (test_mode_rs.get() == false); + let fun_clone = function_data.clone(); + let fun_clone_ = function_data.clone(); + + let pub_code = fun_clone + .published_code + .clone() + .unwrap_or("//Code not published yet".to_string()); + selected_tab_rs + .with(|tab| { + match tab { + CodeTab::PublishedCode => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + { + let fun_pub = fun_clone.clone(); + view! { + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + monaco.editor.create(document.querySelector('.monaco'), {{ + + value: `{pub_code}`, + language: 'javascript', + readOnly: true + }}); + "#, + )} + + </script> + + <Show when=move || { is_test == false }> + + <div class="monaco" style="min-height: 500px"></div> + + </Show> + + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex-row"> + + <TestForm + function_name=fun_pub.function_name.clone() + stage="PUBLISHED".to_string() + handle_submit=move || {} + /> + + </div> + + </Show> + } + .into_view() + } + + </Suspense> + } + .into_view() + } + CodeTab::DraftCode => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + + { + let function_edit = fun_clone_.clone(); + let function_test = fun_clone_.clone(); + let fun_code = fun_clone_.draft_code.clone(); + view! { + <Show when=move || { + editor_mode_rs.get() == false && test_mode_rs.get() == false + }> + <div class="flex-row"> + <FunctionEditor + edit=true + function_name=function_edit.function_name.clone() + function=function_edit.draft_code.clone() + runtime_version=function_edit.draft_runtime_version.clone() + description=function_edit.function_description.clone() + handle_submit=move || { + combined_resource.refetch(); + editor_mode_ws.set(true) + } + /> + </div> + </Show> + + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + window.editor = monaco.editor.create(document.querySelector('.monaco'), {{ + + value: `{fun_code}`, + language: 'javascript', + readOnly: {is_edit} + }}); + + const form = document.getElementById("MyForm"); + form.addEventListener("formdata", e => + {{ + e.formData.set('function', window.editor.getValue()); + }}); + + "#, + )} + + </script> + + <Show when=move || { should_show }> + + <div class="monaco" style="min-height: 500px"></div> + + </Show> + + <Show when=move || { + test_mode_rs.get() == true && editor_mode_rs.get() == true + }> + <div class="flex-row"> + + <TestForm + function_name=function_test.function_name.clone() + stage="DRAFT".to_string() + handle_submit=move || {} + /> + + </div> + + </Show> + } + .into_view() + } + + </Suspense> + } + } + } + }) + }} + + </div> + } + .into_view() + } + None => { + view! { + + <h1>Error fetching function</h1> + } + .into_view() + } + } + }} + + </Transition> + } +} diff --git a/crates/frontend/src/pages/function/function_create.rs b/crates/frontend/src/pages/function/function_create.rs new file mode 100644 index 000000000..0e3f677eb --- /dev/null +++ b/crates/frontend/src/pages/function/function_create.rs @@ -0,0 +1,61 @@ +use crate::components::function_form::function_form::FunctionEditor; +use crate::types::FunctionResponse; +use crate::utils::use_url_base; +use leptos::*; +use leptos_router::use_navigate; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + function: Option<FunctionResponse>, +} + +#[component] +pub fn create_function_view() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + view! { + <div> + + <div class="mt-20 mb-20">Create Function</div> + + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + window.editor = monaco.editor.create(document.querySelector('.monaco'), {{ + + value: [ + 'async function validate() {{', + '}}' + ].join('\n'), + language: 'javascript', + readOnly: false + }}); + + const form = document.getElementById("MyForm"); + form.addEventListener("formdata", e => + {{ + e.formData.set('function', window.editor.getValue()); + }}); + + "#, + )} + + </script> + <FunctionEditor + edit=false + handle_submit=move || { + let tenant = tenant_rs.get(); + let base = use_url_base(); + let redirect_url = format!("{base}/admin/{tenant}/function"); + let navigate = use_navigate(); + navigate(redirect_url.as_str(), Default::default()) + } + /> + + </div> + } + .into_view() +} diff --git a/crates/frontend/src/pages/function/function_list.rs b/crates/frontend/src/pages/function/function_list.rs new file mode 100644 index 000000000..dc524fa56 --- /dev/null +++ b/crates/frontend/src/pages/function/function_list.rs @@ -0,0 +1,175 @@ +use leptos::logging::*; +use leptos::*; + +use chrono::{prelude::Utc, TimeZone}; +use leptos_router::A; +use serde::{Deserialize, Serialize}; + +use crate::components::{ + pagination::pagination::Pagination, stat::stat::Stat, table::table::Table, +}; + +use crate::types::{FunctionResponse, ListFilters}; + +use super::utils::function_table_columns; +use crate::api::fetch_functions; +use serde_json::{json, Map, Value}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + functions: Vec<FunctionResponse>, +} + +#[component] +pub fn function_list() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (filters, set_filters) = create_signal(ListFilters { + status: None, + from_date: Utc.timestamp_opt(0, 0).single(), + to_date: Utc.timestamp_opt(4130561031, 0).single(), + page: Some(1), + count: Some(10), + }); + let table_columns = create_memo(move |_| function_table_columns()); + + let combined_resource: Resource<(String), CombinedResource> = + create_blocking_resource( + move || (tenant_rs.get()), + |(current_tenant)| async move { + let functions_future = fetch_functions(current_tenant.to_string()); + + let functions_result = functions_future.await; + CombinedResource { + functions: functions_result.unwrap_or_else(|_| vec![]), + } + }, + ); + + view! { + <div class="p-8"> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> + <div class="pb-4"> + + {move || { + let value = combined_resource.get(); + let total_items = match value { + Some(v) => std::vec::Vec::len(&v.functions).to_string(), + _ => "0".to_string(), + }; + view! { + <Stat heading="Functions" icon="ri-code-box-fill" number=total_items/> + } + }} + + </div> + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title">Functions</h2> + <div> + + <A + href="create".to_string() + class="btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 btn-link" + > + <button> + Create Function <i class="ri-edit-2-line ml-2"></i> + </button> + </A> + </div> + </div> + <div> + + {move || { + let value = combined_resource.get(); + match value { + Some(v) => { + let data = v + .functions + .iter() + .map(|ele| { + let mut ele_map = json!(ele) + .as_object() + .unwrap() + .to_owned(); + ele_map + .insert( + "published_at".to_string(), + match ele.published_at { + Some(val) => json!(val.format("%v").to_string()), + None => json!("null".to_string()), + }, + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + + <Table + cell_style="".to_string() + rows=data + key_column="id".to_string() + columns=table_columns.get() + /> + } + } + None => { + view! { + + <div>Loading....</div> + } + .into_view() + } + } + }} + + </div> + <div class="mt-2 flex justify-end"> + + {move || { + let current_page = filters.get().page.unwrap_or(0); + let total_pages = match combined_resource.get() { + Some(val) => { + (val.functions.len() as f64 / 10 as f64).ceil() as i64 + } + None => 0, + }; + view! { + <Pagination + current_page=current_page + total_pages=total_pages + next=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p < total_pages => Some(p + 1), + Some(p) => Some(p), + None => None, + } + }); + } + + previous=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p > 1 => Some(p - 1), + Some(p) => Some(p), + None => None, + } + }); + } + /> + } + }} + + </div> + </div> + </div> + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/function/mod.rs b/crates/frontend/src/pages/function/mod.rs new file mode 100644 index 000000000..8ae1f125d --- /dev/null +++ b/crates/frontend/src/pages/function/mod.rs @@ -0,0 +1,4 @@ +pub mod function; +pub mod function_create; +pub mod function_list; +pub mod utils; diff --git a/crates/frontend/src/pages/function/utils.rs b/crates/frontend/src/pages/function/utils.rs new file mode 100644 index 000000000..342c8f704 --- /dev/null +++ b/crates/frontend/src/pages/function/utils.rs @@ -0,0 +1,57 @@ +use crate::components::table::types::Column; +use crate::utils::get_host; +use leptos::*; +use leptos_router::A; +use reqwest::StatusCode; +use serde_json::{Map, Value}; +use std::vec::Vec; + +pub fn function_table_columns() -> Vec<Column> { + vec![ + Column::new( + "function_name".to_string(), + None, + |value: &str, _row: &Map<String, Value>| { + let function_name = value.to_string(); + view! { + <div> + <A href=function_name.clone() class="btn-link"> + {function_name} + </A> + + </div> + } + .into_view() + }, + ), + Column::default("function_description".to_string()), + Column::default("published_runtime_version".to_string()), + Column::default("draft_runtime_version".to_string()), + Column::default("published_at".to_string()), + Column::default("published_by".to_string()), + ] +} + +pub async fn publish_function( + function_name: String, + tenant: String, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}/publish"); + let response = client + .put(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| e.to_string())?; + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index bdb34b612..84fb72802 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -7,3 +7,4 @@ pub mod Experiment; pub mod Home; pub mod NotFound; pub mod experiment_list; +pub mod function; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index cb9cd0290..84ca4e96b 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -2,7 +2,7 @@ use leptos::{ReadSignal, WriteSignal}; use serde::{Deserialize, Serialize}; use std::{str::FromStr, vec::Vec}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use derive_more::{Deref, DerefMut}; use serde_json::{Map, Value}; @@ -45,6 +45,22 @@ pub struct Envs { pub service_prefix: &'static str, } +/*************************Function Type ***************************/ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionResponse { + pub function_name: String, + pub published_code: Option<String>, + pub draft_code: String, + pub function_description: String, + pub published_runtime_version: Option<String>, + pub draft_runtime_version: String, + pub published_at: Option<NaiveDateTime>, + pub draft_edited_at: NaiveDateTime, + pub published_by: Option<String>, + pub draft_edited_by: String, +} + /*********************** Experimentation Types ****************************************/ #[derive( From e5164ded58b83d0b3d014a226df046f057147876 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Mon, 1 Apr 2024 16:05:52 +0530 Subject: [PATCH 327/352] feat: added support for dynamic json schema in frontend --- .../components/default_config_form/default_config_form.rs | 6 ++---- .../src/components/dimension_form/dimension_form.rs | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 4669f9b6d..022fd1147 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -52,7 +52,7 @@ where Ok(boolean) => Value::Bool(boolean), _ => Value::String("Invalid Boolean".to_string()), }, - _ => Value::String(f_value), + _ => Value::from_str(&f_value).expect("Error parsing JSON"), }; let f_schema = match f_type.as_str() { @@ -84,9 +84,7 @@ where "pattern": f_pattern.to_string() }) } - _ => { - json!(f_pattern.to_string()) - } + _ => Value::from_str(&f_pattern).expect("Error parsing JSON"), }; let payload = DefaultConfigCreateReq { diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index 6db1eda77..c98814f9a 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -3,7 +3,8 @@ use super::utils::create_dimension; use crate::components::button::button::Button; use crate::utils::parse_string_to_json_value_vec; use leptos::*; -use serde_json::json; +use serde_json::{json, Value}; +use std::str::FromStr; use web_sys::MouseEvent; #[component] @@ -54,9 +55,7 @@ where "pattern": f_pattern.to_string() }) } - _ => { - json!(f_pattern.to_string()) - } + _ => Value::from_str(&f_pattern).expect("Error parsing JSON"), }; let payload = DimensionCreateReq { From de71bb2f08fbca604101f95197813dfd5b223ce5 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Fri, 5 Apr 2024 11:43:18 +0530 Subject: [PATCH 328/352] fix: empty key filters should return all keys --- crates/cac_client/src/interface.rs | 12 ++++++++++-- crates/cac_client/src/lib.rs | 15 +++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs index 4686ed820..f8e4f12c3 100644 --- a/crates/cac_client/src/interface.rs +++ b/crates/cac_client/src/interface.rs @@ -207,7 +207,11 @@ pub extern "C" fn get_resolved_config( ); let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); - let key_vector = key.split("|").map(str::to_string).collect(); + let key_vector = if key.is_empty() { + vec![] + } else { + key.split("|").map(str::to_string).collect() + }; let query = unwrap_safe!(cstring_to_rstring(query), return std::ptr::null()); let merge_strategem = @@ -248,7 +252,11 @@ pub extern "C" fn get_default_config( keys: *const c_char, ) -> *const c_char { let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); - let key_vector = key.split("|").map(str::to_string).collect(); + let key_vector = if key.is_empty() { + vec![] + } else { + key.split("|").map(str::to_string).collect() + }; unwrap_safe!( unsafe { (*client).get_default_config(key_vector).map(|ov| { diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 8a150b826..809363da9 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -203,14 +203,13 @@ impl Client { ) -> Result<Map<String, Value>, String> { let configs = self.config.read().map_err(|e| e.to_string())?; let default_configs = configs.default_configs.clone(); - let default_configs = if keys.len() > 0 { - default_configs - .into_iter() - .filter(|(item, _)| keys.contains(item)) - .collect::<Map<String, Value>>() - } else { - default_configs - }; + if keys.is_empty() { + return Ok(default_configs); + } + let default_configs = default_configs + .into_iter() + .filter(|(item, _)| keys.contains(item)) + .collect::<Map<String, Value>>(); Ok(default_configs) } } From 53af262b2f2a8559c2d43c458b423809b5a026da Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Thu, 28 Mar 2024 12:23:26 +0530 Subject: [PATCH 329/352] feat: -js-secure-sandbox --- .../src/api/context/helpers.rs | 2 +- .../src/api/default_config/handlers.rs | 9 +- .../src/api/functions/handlers.rs | 8 +- .../src/validation_functions.rs | 164 +++++++++++------- .../context-aware-config/tests/cac_tests.rs | 39 ++--- 5 files changed, 120 insertions(+), 102 deletions(-) diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context-aware-config/src/api/context/helpers.rs index 844581297..60b010202 100644 --- a/crates/context-aware-config/src/api/context/helpers.rs +++ b/crates/context-aware-config/src/api/context/helpers.rs @@ -116,7 +116,7 @@ fn validate_value_with_function( ) -> anyhow::Result<()> { let base64_decoded = BASE64_STANDARD.decode(function)?; let utf8_decoded = str::from_utf8(&base64_decoded)?; - if let Err((e, stdout)) = execute_fn(&utf8_decoded, fun_name, key, value.to_owned()) { + if let Err((e, stdout)) = execute_fn(&utf8_decoded, key, value.to_owned()) { log::error!("function validation failed for {key} with error: {e}"); return Err(anyhow!(json!({ "message": format!("function validation failed for {} with error {}", key, e), "stdout": stdout diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index 78a14ca49..eb023281d 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -142,12 +142,9 @@ async fn create( ErrorInternalServerError(json!({"message": e.to_string()})) })?; - if let Err((e, stdout)) = execute_fn( - &utf8_decoded, - &f_name, - &key, - default_config.value.to_owned(), - ) { + if let Err((e, stdout)) = + execute_fn(&utf8_decoded, &key, default_config.value.to_owned()) + { log::info!("function validation failed for {key} with error: {e}"); return Err(ErrorBadRequest(json!({ "message": format!( "function validation failed with error: {}", e), "stdout": stdout diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index 1142826de..b3b635c63 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -48,7 +48,7 @@ async fn create( let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - if let Err(e) = compile_fn(&req.function, &req.function_name) { + if let Err(e) = compile_fn(&req.function) { return Err(ErrorBadRequest(json!({ "message": e }))); } @@ -127,7 +127,7 @@ async fn update( // Function Linter Check if let Some(function) = &req.function { - if let Err(e) = compile_fn(function, &f_name) { + if let Err(e) = compile_fn(function) { return Err(ErrorBadRequest(json!({ "message": e }))); } } @@ -260,9 +260,9 @@ async fn test( }; decode_function(&mut function)?; let result = match path_params.stage { - Stage::DRAFT => execute_fn(&function.draft_code, fun_name, &req.key, req.value), + Stage::DRAFT => execute_fn(&function.draft_code, &req.key, req.value), Stage::PUBLISHED => match function.published_code { - Some(code) => execute_fn(&code, fun_name, &req.key, req.value), + Some(code) => execute_fn(&code, &req.key, req.value), None => { log::error!("Function test failed: function not published yet"); Err(( diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 6380e53a3..4115f943c 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -2,77 +2,116 @@ use serde_json::{json, Value}; use std::process::Command; use std::str; -const IMPORT_CODE: &str = r#" - /*eslint no-unused-vars: "off"*/ - /*eslint no-extra-semi: "off"*/ - const axios = require("./target/node_modules/axios"); - "#; +fn type_check_validate(code_str: &str) -> String { + format!( + r#"const vm = require("node:vm") + const axios = require("axios") + const script = new vm.Script(\` + + {} -const EXIT_LOGIC_CODE: &str = r#" - if (fun_value != true) { - console.error(fun_value) - process.exit(1); - }; - "#; + if(typeof(validate)!="function") + {{ + throw Error("validate is not of function type") + }}\`); + + script.runInNewContext({{axios,console}}, {{ timeout: 1500}}); + "#, + code_str + ) +} -const ES_LINT_CODE: &str = r#" - const {ESLint} = require("./target/node_modules/eslint"); - const linter = new ESLint({ - useEslintrc: false, - overrideConfig: { - extends: ["eslint:recommended"], - parserOptions: { - sourceType: "module", - ecmaVersion: "latest", - }, - env: { - browser: true, - commonjs: true, - } - }, - }); +fn execute_validate_fun(code_str: &str, val: Value) -> String { + format!( + r#" + const vm = require("node:vm") + const axios = require("axios") + const script = new vm.Script(\` + + {} + Promise.resolve(validate({})).then((output) => {{ + + if(output!=true){{ + throw new Error("The function did not return true as expected. Check the conditions or logic inside the function.") + }} + return output; + }}).catch((err)=> {{ + throw new Error(err) + }});\`); + + script.runInNewContext({{axios,console,process}}, {{ timeout: 1500}}); + "#, + code_str, val + ) +} - linter.lintText(codeToLint).then((results) => { - var err_count = 0; - var err_msgs = []; - for (var err_obj of results) { - console.log(err_obj.messages); - err_count = err_count + err_obj.errorCount; - err_msgs.push(err_obj.messages); - } - - if (err_count > 0) { - console.error(err_msgs); - process.exit(1); - } - }).catch((error) => { - console.error(error); +fn generate_code(code_str: &str) -> String { + format!( + r#" + const {{ Worker, isMainThread, threadId }} = require("node:worker_threads"); + + if (isMainThread) {{ + + // starting worker thread , making separated from the main thread + function runService() {{ + return new Promise((resolve, reject) => {{ + const worker = new Worker( + `{}`,{{eval:true}} + ); + worker.on("message", (msg) => {{ + console.log(msg); + }}); + worker.on("error", (err) => {{ + clearTimeout(tl); + console.error(err.message); process.exit(1); - }); + }}); + worker.on("exit", (code) => {{ + clearTimeout(tl); + if (code !== 0) {{ + console.error(`Script stopped with exit code ${{code}}`); + process.exit(code); + }} else {{ + worker.terminate(); + }} + }}); - "#; + function timelimit() {{ + worker.terminate(); + throw new Error("time limit exceeded"); + }} -fn runtime_wrapper(function_name: &str, key: &str, value: Value) -> String { - let fun_call: String = format!( - "const fun_value = {}({});", - function_name, - json!({ - "key": key, - "value": value - }) - ); - fun_call + EXIT_LOGIC_CODE + // terminate worker thread if execution time exceed 2 secs + + var tl = setTimeout(timelimit, 2000); + }}); + }} + + async function run() {{ + const result = await runService(); + console.log("result output: ", result); + }} + run().catch((err) => console.error(err)); + }} + + "#, + code_str + ) } pub fn execute_fn( code_str: &str, - fun_name: &str, key: &str, value: Value, ) -> Result<String, (String, Option<String>)> { + let fun_val = json!({ + "key": key, + "value": value + }); + let exec_code = execute_validate_fun(code_str, fun_val); let output = Command::new("node") .arg("-e") - .arg(IMPORT_CODE.to_string() + code_str + &runtime_wrapper(fun_name, key, value)) + .arg(generate_code(&exec_code)) .output(); log::trace!("{}", format!("validation function output : {:?}", output)); match output { @@ -100,18 +139,13 @@ pub fn execute_fn( } } -fn eslint_logic(code_str: &str, fun_name: &str) -> String { - let code = - IMPORT_CODE.to_string() + code_str + &format!("console.log({});", fun_name); - let fun_call: String = format!("\nconst codeToLint = {};", json!(code)); - fun_call + ES_LINT_CODE -} - -pub fn compile_fn(code_str: &str, fun_name: &str) -> Result<(), String> { +pub fn compile_fn(code_str: &str) -> Result<(), String> { + let type_check_code = type_check_validate(code_str); let output = Command::new("node") .arg("-e") - .arg(eslint_logic(code_str, fun_name)) + .arg(generate_code(&type_check_code)) .output(); + log::trace!("{}", format!("validation function output : {:?}", output)); match output { Ok(val) => { diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index 35f7395c2..e9e0fa785 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -4,50 +4,37 @@ use serde_json::json; // #[test] //todo : currently there is issue in running this test fn test_execute_fn() { let code_ok = r#" - function test_fun() { + function validate() { return true; }; "#; let execute_code_error = r#" - function test_fun() { + function validate() { return false; } "#; let compile_code_error = r#" - function test_fun( { + function validate( { return true; } "#; - let err_execute = match execute_fn( - &(execute_code_error.to_owned()), - &"test_fun".to_owned(), - "test", - json!(10), - ) { - Ok(_) => false, - Err((e, stdout)) => e.contains("Bad schema"), - }; - let err_compile = - match compile_fn(&(compile_code_error.to_owned()), &"test_fun".to_owned()) { - Ok(()) => false, - Err(e) => e.contains("Bad schema"), + let err_execute = + match execute_fn(&(execute_code_error.to_owned()), "test", json!(10)) { + Ok(_) => false, + Err((e, stdout)) => e.contains("Bad schema"), }; + let err_compile = match compile_fn(&(compile_code_error.to_owned())) { + Ok(()) => false, + Err(e) => e.contains("Bad schema"), + }; assert_eq!( - execute_fn( - &(code_ok.to_owned()), - &"test_fun".to_owned(), - "test", - json!(10) - ), + execute_fn(&(code_ok.to_owned()), "test", json!(10)), Ok("true".to_string()) ); assert_eq!(err_execute, true); - assert_eq!( - compile_fn(&(code_ok.to_owned()), &"test_fun".to_owned()), - Ok(()) - ); + assert_eq!(compile_fn(&(code_ok.to_owned())), Ok(())); assert_eq!(err_compile, true); } From 233bf7d8dfb9272069e086038198f7e88c56593c Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Fri, 5 Apr 2024 07:54:38 +0000 Subject: [PATCH 330/352] chore(version): v0.35.0 [skip ci] --- CHANGELOG.md | 17 +++++++++++++++++ Cargo.lock | 8 ++++---- crates/cac_client/CHANGELOG.md | 11 +++++++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- crates/experimentation-platform/CHANGELOG.md | 8 ++++++++ crates/experimentation-platform/Cargo.toml | 2 +- crates/frontend/CHANGELOG.md | 7 +++++++ crates/frontend/Cargo.toml | 2 +- 10 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ac53a65..86c3846e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.35.0 - 2024-04-05 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.0 +- experimentation-platform bumped to experimentation-platform-v0.12.0 +- frontend bumped to frontend-v0.3.0 +- cac_client bumped to cac_client-v0.8.0 +### Global changes +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26101] client interface improvements - (d606cb1) - Kartik +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik +#### Miscellaneous Chores +- rename superposition to experimentation - (9efa3cb) - Kartik + +- - - + ## v0.34.2 - 2024-03-27 ### Package updates - context-aware-config bumped to context-aware-config-v0.24.2 diff --git a/Cargo.lock b/Cargo.lock index 6785f2a91..94767c53b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.7.0" +version = "0.8.0" dependencies = [ "actix-web", "cbindgen", @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.24.2" +version = "0.25.0" dependencies = [ "actix", "actix-cors", @@ -1398,7 +1398,7 @@ dependencies = [ [[package]] name = "experimentation-platform" -version = "0.11.0" +version = "0.12.0" dependencies = [ "actix", "actix-web", @@ -1523,7 +1523,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.2.1" +version = "0.3.0" dependencies = [ "actix-files", "actix-web", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 3eb485470..ceaffeb61 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.8.0 - 2024-04-05 +#### Bug Fixes +- [PICAF-26101] empty key filters should return all keys - (f9dd889) - Kartik +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26101] client interface improvements - (d606cb1) - Kartik +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik + +- - - + ## cac_client-v0.7.0 - 2024-03-18 #### Documentation - PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index ff7ef954c..c08c40b9f 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.7.0" +version = "0.8.0" edition = "2021" build = "build.rs" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 85af6e8a4..609490376 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.25.0 - 2024-04-05 +#### Features +- PICAF-26168-js-secure-sandbox - (566f8be) - Pratik Mishra + +- - - + ## context-aware-config-v0.24.2 - 2024-03-27 #### Bug Fixes - PICAF-26454 JS validator functions to take config value and key - (656fe39) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 7f4fed41d..3bd80f1df 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.24.2" +version = "0.25.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation-platform/CHANGELOG.md index f8cd72cbb..b66930d51 100644 --- a/crates/experimentation-platform/CHANGELOG.md +++ b/crates/experimentation-platform/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## experimentation-platform-v0.12.0 - 2024-04-05 +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik + +- - - + ## experimentation-platform-v0.11.0 - 2024-03-18 #### Documentation - PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index ed31dd262..b9e08f6a8 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "experimentation-platform" -version = "0.11.0" +version = "0.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index 7c7175148..9e8d6689f 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.3.0 - 2024-04-05 +#### Features +- [PICAF-26522] added support for dynamic json schema in frontend - (b02f08f) - Saurav Suman +- [PICAF-26346] function ui - (9360aca) - Pratik Mishra + +- - - + ## frontend-v0.2.1 - 2024-03-21 #### Bug Fixes - PICAF-26324 added type for condition - (3607336) - Saurav CV diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 83abd2873..8b8932b6f 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.2.1" +version = "0.3.0" edition = "2021" [lib] From c86b89a3663c711b7b4542ce6b488296889b8307 Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Mon, 11 Mar 2024 19:05:56 +0530 Subject: [PATCH 331/352] feat: added decimal support in context and override form and fixed dimension modal --- .../src/components/context_form/utils.rs | 64 +++++++--------- .../default_config_form.rs | 2 +- .../dimension_form/dimension_form.rs | 54 ++++++++------ .../components/override_form/override_form.rs | 37 +--------- crates/frontend/src/utils.rs | 73 ++++++++++++++++++- 5 files changed, 134 insertions(+), 96 deletions(-) diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 694a1a52e..f78104d86 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,38 +1,17 @@ use crate::types::Dimension; -use crate::utils::get_host; +use crate::utils::{get_config_value, get_host, ConfigType}; +use anyhow::{Error, Result}; use reqwest::StatusCode; -use serde_json::{json, Map, Value}; +use serde_json::{json, Map, Number, Value}; +use std::io::ErrorKind; use std::str::FromStr; -pub fn get_dimension_type(dimensions: Vec<Dimension>, dimension_name: &str) -> String { - let dimension = dimensions - .iter() - .find(|&dimension| dimension.dimension == dimension_name.to_string()); - let schema = &dimension.unwrap().schema; - let schema_type = schema.get("type").unwrap(); - schema_type.to_string() -} - pub fn get_condition_schema( var: &str, op: &str, val: &str, dimensions: Vec<Dimension>, -) -> Value { - let dimension_value = |variable: &str, val: &str, dimensions: &Vec<Dimension>| { - let dimension_type = get_dimension_type(dimensions.clone(), variable); - match dimension_type.replace("\"", "").as_str() { - "boolean" => match bool::from_str(val) { - Ok(boolean) => Value::Bool(boolean), - _ => Value::String("Invalid Boolean".to_string()), - }, - "number" => match val.parse::<i64>() { - Ok(number) => Value::Number(number.into()), - Err(_) => Value::String(val.to_string()), - }, - _ => Value::String(val.to_string()), - } - }; +) -> Result<Value, String> { match op { "<=" => { let mut split_value = val.split(','); @@ -40,25 +19,38 @@ pub fn get_condition_schema( let first_operand = split_value.next().unwrap().trim().parse::<i64>().unwrap(); - let dimension_val = - dimension_value(var, split_value.next().unwrap().trim(), &dimensions); + let dimension_val = get_config_value( + var, + split_value.next().unwrap().trim(), + &dimensions + .into_iter() + .map(ConfigType::Dimension) + .collect::<Vec<_>>(), + ); - json!({ + Ok(json!({ op: [ first_operand, { "var": var }, - dimension_val + dimension_val.expect("can't parse dimension value") ] - }) + })) } _ => { - let dimension_val = dimension_value(var, val, &dimensions); - json!({ + let dimension_val = get_config_value( + var, + val, + &dimensions + .into_iter() + .map(ConfigType::Dimension) + .collect::<Vec<_>>(), + ); + Ok(json!({ op: [ {"var": var}, - dimension_val + dimension_val.expect("can't parse dimension value") ] - }) + })) } } } @@ -70,7 +62,7 @@ pub fn construct_context( let condition_schemas = conditions .iter() .map(|(variable, operator, value)| { - get_condition_schema(variable, operator, value, dimensions.clone()) + get_condition_schema(variable, operator, value, dimensions.clone()).unwrap() }) .collect::<Vec<Value>>(); diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 022fd1147..027ed69db 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -181,7 +181,7 @@ where value="decimal" selected=move || { config_type.get() == "decimal".to_string() } > - "Decimal" + "Decimal (16 digits)" </option> <option value="boolean" diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index c98814f9a..f0eee2b08 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -43,6 +43,17 @@ where "type": f_type.to_string() }) } + "decimal" => { + json!({ + "type": "number".to_string(), + }) + } + "boolean" => { + json!({ + "type": "boolean".to_string(), + } + ) + } "enum" => { json!({ "type": "string", @@ -110,6 +121,12 @@ where "number" => { set_dimension_type.set("number".to_string()); } + "decimal" => { + set_dimension_type.set("decimal".to_string()); + } + "boolean" => { + set_dimension_type.set("boolean".to_string()); + } "enum" => { set_dimension_type.set("enum".to_string()); set_dimension_pattern @@ -139,6 +156,18 @@ where "Number" </option> + <option + value="decimal" + selected= move || {dimension_type.get() == "decimal".to_string()} + > + "Decimal (Max Value : 1.7976931348623157e+308)" + </option> + <option + value= "boolean" + selected= move || {dimension_type.get() == "boolean".to_string()} + > + "Boolean" + </option> <option value="enum" selected=move || { dimension_type.get() == "enum".to_string() } @@ -161,7 +190,6 @@ where {move || { view! { - <Show when=move || (dimension_type.get() == "number")> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono">Priority</span> @@ -183,30 +211,8 @@ where /> </div> - </Show> - <Show when=move || (show_labels.get() && (dimension_type.get() != "number"))> - <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> - </label> - <input - type="Number" - placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - value=priority.get() - on:change=move |ev| { - logging::log!( - "{:?}", event_target_value(& ev).parse::< u16 > () - ); - match event_target_value(&ev).parse::<u16>() { - Ok(i_prio) => set_priority.set(i_prio), - Err(e) => logging::log!("{e}"), - }; - } - /> - - </div> + <Show when=move || (show_labels.get() && ((dimension_type.get() == "enum") || (dimension_type.get() == "pattern") || (dimension_type.get() == "other")))> <div class="form-control"> <label class="label font-mono"> <span class="label-text text-gray-700 font-mono"> diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs index b4a4a8b6e..ca064aec9 100644 --- a/crates/frontend/src/components/override_form/override_form.rs +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -1,24 +1,12 @@ use crate::{ components::dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, types::DefaultConfig, + utils::{get_config_value, ConfigType}, }; use leptos::*; use serde_json::{json, Map, Value}; -use std::str::FromStr; use web_sys::MouseEvent; -pub fn get_default_config_type( - default_configs: Vec<DefaultConfig>, - key_name: &str, -) -> String { - let default_config = default_configs - .iter() - .find(|&default_conf| default_conf.key == key_name.to_string()); - let schema = &default_config.unwrap().schema; - let schema_type = schema.get("type").unwrap(); - schema_type.to_string() -} - #[component] pub fn override_form<NF>( overrides: Map<String, Value>, @@ -48,22 +36,6 @@ where logging::log!("{:?}", overrides.get()); }; - let default_config_value = - |name: &str, val: &str, default_config: &Vec<DefaultConfig>| { - let dimension_type = get_default_config_type(default_config.clone(), name); - match dimension_type.replace("\"", "").as_str() { - "boolean" => match bool::from_str(val) { - Ok(boolean) => Value::Bool(boolean), - _ => Value::String("Invalid Boolean".to_string()), - }, - "number" => match val.parse::<i64>() { - Ok(number) => Value::Number(number.into()), - Err(_) => Value::String(val.to_string()), - }, - _ => Value::String(val.to_string()), - } - }; - let handle_config_key_select = move |default_config: DefaultConfig| { let config_key = default_config.key; set_overrides.update(|value| { @@ -128,12 +100,11 @@ where value=config_value on:input=move |event| { let input_value = event_target_value(&event); - let default_config_val = default_config_value( + let default_config_val = get_config_value( &config_key_value, &input_value, - &default_config.get_value(), - ); - logging::log!("Default Config: {}", default_config_val); + &default_config.get_value().into_iter().map(ConfigType::DefaultConfig).collect::<Vec<_>>(), + ).expect("can't parse default config key"); set_overrides .update(|curr_overrides| { curr_overrides diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index 48fe188ef..fc4cb4faf 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,8 +1,9 @@ use std::env; -use crate::types::Envs; +use crate::types::{DefaultConfig, Dimension, Envs}; use leptos::*; -use serde_json::Value; +use serde_json::{Number, Value}; +use std::str::FromStr; use url::Url; use wasm_bindgen::JsCast; @@ -274,3 +275,71 @@ pub fn check_url_and_return_val(s: String) -> String { Err(_) => s, } } + +pub enum ConfigType { + DefaultConfig(DefaultConfig), + Dimension(Dimension), +} + +pub enum ConfigValueType { + Boolean, + Number, + String, + Other, +} + +pub fn get_config_type( + configs: &[ConfigType], + key_name: &str, +) -> Option<ConfigValueType> { + let config = configs.iter().find(|conf| match conf { + ConfigType::DefaultConfig(default_conf) => default_conf.key == key_name, + ConfigType::Dimension(dimension) => dimension.dimension == key_name, + }); + + let types_mapping = |type_str: Option<&str>| match type_str { + Some("boolean") => ConfigValueType::Boolean, + Some("number") => ConfigValueType::Number, + Some("string") => ConfigValueType::String, + _ => ConfigValueType::Other, + }; + + config.and_then(|config| match config { + ConfigType::DefaultConfig(default_conf) => default_conf + .schema + .get("type") + .map(|t| types_mapping(t.as_str())), + ConfigType::Dimension(dimension) => dimension + .schema + .get("type") + .map(|t| types_mapping(t.as_str())), + }) +} + +pub fn get_config_value( + name: &str, + val: &str, + configs: &[ConfigType], +) -> Result<Value, String> { + let config_value_type = get_config_type(configs, name); + match config_value_type { + Some(ConfigValueType::Boolean) => bool::from_str(val) + .map(Value::Bool) + .map_err(|_| "Invalid boolean".to_string()), + Some(ConfigValueType::Number) => val + .parse::<i64>() + .map(|number| Value::Number(number.into())) + .or_else(|_| { + f64::from_str(val) + .ok() + .and_then(|num| Number::from_f64(num).map(Value::Number)) + .ok_or_else(|| { + "Invalid decimal format or precision issue".to_string() + }) + }), + Some(ConfigValueType::String) => Ok(Value::String(val.to_string())), + Some(ConfigValueType::Other) | None => { + Value::from_str(val).map_err(|err| format!("Error parsing JSON: {}", err)) + } + } +} From cdc46ce5097a7bd373bbc943c3e8966d555fe2f6 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Fri, 5 Apr 2024 11:36:59 +0000 Subject: [PATCH 332/352] chore(version): v0.36.0 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/frontend/CHANGELOG.md | 6 ++++++ crates/frontend/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c3846e2..7c00b7b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.36.0 - 2024-04-05 +### Package updates +- frontend bumped to frontend-v0.4.0 +### Global changes + +- - - + ## v0.35.0 - 2024-04-05 ### Package updates - context-aware-config bumped to context-aware-config-v0.25.0 diff --git a/Cargo.lock b/Cargo.lock index 94767c53b..9492330aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,7 +1523,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix-files", "actix-web", diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index 9e8d6689f..16b44c826 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.4.0 - 2024-04-05 +#### Features +- [PICAF-26360] added decimal support in context and override form and fixed dimension modal - (3f1f998) - Saurav Suman + +- - - + ## frontend-v0.3.0 - 2024-04-05 #### Features - [PICAF-26522] added support for dynamic json schema in frontend - (b02f08f) - Saurav Suman diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 8b8932b6f..cb198bd8e 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.3.0" +version = "0.4.0" edition = "2021" [lib] From 606947c03a6eec77b59a75dec6ad2dab1a36f155 Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Fri, 5 Apr 2024 17:48:20 +0530 Subject: [PATCH 333/352] fix: add path to node_modules --- crates/context-aware-config/src/validation_functions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 4115f943c..291096588 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -5,7 +5,7 @@ use std::str; fn type_check_validate(code_str: &str) -> String { format!( r#"const vm = require("node:vm") - const axios = require("axios") + const axios = require("./target/node_modules/axios") const script = new vm.Script(\` {} @@ -25,7 +25,7 @@ fn execute_validate_fun(code_str: &str, val: Value) -> String { format!( r#" const vm = require("node:vm") - const axios = require("axios") + const axios = require("./target/node_modules/axios") const script = new vm.Script(\` {} From ebba320f35303cd2550083fc9ce289a6d98b2cd2 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Mon, 8 Apr 2024 04:05:55 +0000 Subject: [PATCH 334/352] chore(version): v0.36.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c00b7b03..d23e3d01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.36.1 - 2024-04-08 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.1 +### Global changes + +- - - + ## v0.36.0 - 2024-04-05 ### Package updates - frontend bumped to frontend-v0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 9492330aa..d40ec2580 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.25.0" +version = "0.25.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 609490376..4644dfa3b 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.25.1 - 2024-04-08 +#### Bug Fixes +- [PICAF-26346] add path to node_modules - (c4bc7b6) - Pratik Mishra + +- - - + ## context-aware-config-v0.25.0 - 2024-04-05 #### Features - PICAF-26168-js-secure-sandbox - (566f8be) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 3bd80f1df..f554441a5 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.25.0" +version = "0.25.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From d17072786706b0a9a5ce191b35ed18a366f30549 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Thu, 21 Mar 2024 17:26:56 +0530 Subject: [PATCH 335/352] feat: added new result, error type and error macros --- Cargo.lock | 9 ++- Cargo.toml | 1 + crates/service-utils/Cargo.toml | 1 + crates/service-utils/src/lib.rs | 2 + crates/service-utils/src/macros.rs | 55 ++++++++++++++ crates/service-utils/src/result.rs | 113 +++++++++++++++++++++++++++++ 6 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 crates/service-utils/src/macros.rs create mode 100644 crates/service-utils/src/result.rs diff --git a/Cargo.lock b/Cargo.lock index d40ec2580..b2dbda40c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3557,6 +3557,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "thiserror", "urlencoding", ] @@ -3844,18 +3845,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f8ec0c4af..411d8c88f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,3 +58,4 @@ tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.g leptos = { version = "0.5.2" } leptos_meta = { version = "0.5.2" } leptos_router = { version = "0.5.2" } +thiserror = { version = "1.0.57" } \ No newline at end of file diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index fefa91d02..71928d155 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -33,3 +33,4 @@ serde_json = { workspace = true } derive_more = { workspace = true } dashboard-auth = { workspace = true } reqwest = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/service-utils/src/lib.rs b/crates/service-utils/src/lib.rs index b8d22b7bf..96e392401 100644 --- a/crates/service-utils/src/lib.rs +++ b/crates/service-utils/src/lib.rs @@ -5,3 +5,5 @@ pub mod helpers; pub mod middlewares; pub mod service; pub mod types; +pub mod result; +pub mod macros; \ No newline at end of file diff --git a/crates/service-utils/src/macros.rs b/crates/service-utils/src/macros.rs new file mode 100644 index 000000000..a125c6c33 --- /dev/null +++ b/crates/service-utils/src/macros.rs @@ -0,0 +1,55 @@ +#[macro_export] +macro_rules! bad_argument { + ($msg: literal) => { + service_utils::result::AppError::BadArgument($msg.into()) + }; + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::BadArgument(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::BadArgument($err.to_string()) + }; +} + +#[macro_export] +macro_rules! validation_error { + ($msg: literal) => { + service_utils::result::AppError::ValidationError($msg.into()) + }; + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::ValidationError(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::ValidationError($err.to_string()) + }; +} + +#[macro_export] +macro_rules! unexpected_error { + ($msg: literal) => { + service_utils::result::AppError::UnexpectedError(anyhow::anyhow!($msg)) + }; + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::UnexpectedError(anyhow::anyhow!(format!($msg, $($args)*))) + }; + ($err: tt) => { + service_utils::result::AppError::UnexpectedError(anyhow::anyhow!($err.to_string())) + }; +} + +#[macro_export] +macro_rules! db_error { + ($error: expr) => { + service_utils::result::AppError::DbError($error) + }; +} + +#[macro_export] +macro_rules! server_error { + ($status: expr, $msg: expr) => { + service_utils::result::AppError::ServerError(service_utils::result::ServerError { + status_code: $status, + message: $msg, + }) + }; +} \ No newline at end of file diff --git a/crates/service-utils/src/result.rs b/crates/service-utils/src/result.rs new file mode 100644 index 000000000..a3ca0a542 --- /dev/null +++ b/crates/service-utils/src/result.rs @@ -0,0 +1,113 @@ +use actix_web::{ + error, + http::{header::ContentType, StatusCode}, + HttpResponse, +}; +use derive_more::Display; +use serde_json::json; +use thiserror::Error as this_error; + +#[derive(this_error)] +pub enum AppError { + #[error("validation failed ( `{0}` )")] + ValidationError(String), + #[error("bad arguments ( `{0}` )")] + BadArgument(String), + #[error(transparent)] + DbError(#[from] diesel::result::Error), + #[error(transparent)] + ServerError(#[from] ServerError), + #[error(transparent)] + UnexpectedError(anyhow::Error), +} + +#[derive(Debug, this_error, Display, Clone)] +#[display( + fmt = "server returned an error: {} with status code {}", + message, + status_code +)] +pub struct ServerError { + pub message: String, + pub status_code: StatusCode, +} + +pub type Result<T> = core::result::Result<T, AppError>; + +impl AppError { + fn generate_err_response(code: StatusCode, msg: &str) -> HttpResponse { + let response = json!({ "message" : msg }); + HttpResponse::build(code) + .insert_header(ContentType::json()) + .json(response) + } +} + +impl error::ResponseError for AppError { + fn error_response(&self) -> HttpResponse { + use diesel::result::DatabaseErrorKind as diesel_error_kind; + use diesel::result::Error as diesel_error; + + log::error!("{}", self); + match self { + AppError::ValidationError(msg) => { + Self::generate_err_response(StatusCode::BAD_REQUEST, msg) + } + AppError::BadArgument(msg) => { + Self::generate_err_response(StatusCode::BAD_REQUEST, msg) + } + AppError::UnexpectedError(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong", + ), + + AppError::DbError(diesel_error::InvalidCString(_)) => { + Self::generate_err_response( + StatusCode::SERVICE_UNAVAILABLE, + "Something went wrong", + ) + } + + AppError::DbError(diesel_error::NotFound) => Self::generate_err_response( + StatusCode::NOT_FOUND, + "No records found. Please refine or correct your search parameters", + ), + + AppError::DbError(diesel_error::DatabaseError( + diesel_error_kind::UniqueViolation + | diesel_error_kind::CheckViolation + | diesel_error_kind::NotNullViolation + | diesel_error_kind::ForeignKeyViolation, + error, + )) => Self::generate_err_response(StatusCode::CONFLICT, error.message()), + + AppError::DbError(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong", + ), + + AppError::ServerError(error) => { + Self::generate_err_response(error.status_code, &error.message) + } + } + } +} + +fn error_chain_fmt( + e: &dyn std::error::Error, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + writeln!(f, "{}\n", e)?; + let mut current = e.source(); + while let Some(cause) = current { + writeln!(f, "Caused by:\n\t{}", cause)?; + current = cause.source(); + } + Ok(()) +} + +impl std::fmt::Debug for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} \ No newline at end of file From a99f4afb8b803f2d6ace5c6553845ecf8aff1777 Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev <shubhranshu.sanjeev@juspay.in> Date: Sat, 30 Mar 2024 17:47:38 +0530 Subject: [PATCH 336/352] refactor: refactored service to use new error type and better error handling --- Cargo.lock | 2 + .../src/api/audit_log/handlers.rs | 4 +- .../src/api/config/handlers.rs | 87 +++-- .../src/api/config/helpers.rs | 12 +- .../src/api/context/handlers.rs | 279 ++++++--------- .../src/api/context/helpers.rs | 38 +- .../src/api/context/mod.rs | 2 +- .../src/api/context/types.rs | 12 - .../src/api/default_config/handlers.rs | 86 ++--- .../src/api/dimension/handlers.rs | 41 ++- .../src/api/dimension/utils.rs | 3 +- .../src/api/functions/handlers.rs | 170 ++++----- .../src/api/functions/helpers.rs | 20 +- crates/context-aware-config/src/helpers.rs | 41 ++- .../src/validation_functions.rs | 23 +- .../context-aware-config/tests/cac_tests.rs | 8 +- crates/experimentation-platform/Cargo.toml | 1 + .../src/api/experiments/handlers.rs | 338 +++++++++--------- .../src/api/experiments/helpers.rs | 71 ++-- .../tests/experimentation_tests.rs | 2 +- crates/external/Cargo.toml | 1 + .../external/src/api/external_api/handlers.rs | 10 +- .../external/src/api/external_api/helpers.rs | 16 +- .../src/components/experiment/experiment.rs | 2 +- crates/frontend/src/lib.rs | 4 +- crates/service-utils/src/errors/mod.rs | 1 - crates/service-utils/src/errors/types.rs | 151 -------- crates/service-utils/src/helpers.rs | 75 ++-- crates/service-utils/src/lib.rs | 6 +- crates/service-utils/src/macros.rs | 33 +- crates/service-utils/src/result.rs | 32 +- crates/service-utils/src/types.rs | 1 - 32 files changed, 651 insertions(+), 921 deletions(-) delete mode 100644 crates/service-utils/src/errors/mod.rs delete mode 100644 crates/service-utils/src/errors/types.rs delete mode 100644 crates/service-utils/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index b2dbda40c..687382bc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1402,6 +1402,7 @@ version = "0.12.0" dependencies = [ "actix", "actix-web", + "anyhow", "chrono", "dashboard-auth", "derive_more", @@ -1441,6 +1442,7 @@ version = "0.4.0" dependencies = [ "actix", "actix-web", + "anyhow", "chrono", "dashboard-auth", "dotenv", diff --git a/crates/context-aware-config/src/api/audit_log/handlers.rs b/crates/context-aware-config/src/api/audit_log/handlers.rs index 363a51b5b..57f88e32c 100644 --- a/crates/context-aware-config/src/api/audit_log/handlers.rs +++ b/crates/context-aware-config/src/api/audit_log/handlers.rs @@ -2,7 +2,7 @@ use actix_web::{get, web::Query, HttpResponse, Scope}; use chrono::{Duration, Utc}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::json; -use service_utils::{service::types::DbConnection, types as app}; +use service_utils::{result as superposition, service::types::DbConnection}; use crate::{api::audit_log::types::AuditQueryFilters, db::models::EventLog}; @@ -16,7 +16,7 @@ pub fn endpoints() -> Scope { async fn get_audit_logs( filters: Query<AuditQueryFilters>, db_conn: DbConnection, -) -> app::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let query_builder = |filters: &AuditQueryFilters| { diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index cda13ac90..1d2c0936c 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -10,9 +10,7 @@ use crate::db::schema::{ contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, }; use actix_http::header::{HeaderName, HeaderValue}; -use actix_web::{ - error::ErrorBadRequest, get, web::Query, HttpRequest, HttpResponse, Scope, -}; +use actix_web::{get, web::Query, HttpRequest, HttpResponse, Scope}; use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy}; use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; use diesel::{ @@ -20,10 +18,11 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use serde_json::{json, Map, Value, Value::Null}; -use service_utils::{ - errors::types::Error as err, helpers::ToActixErr, service::types::DbConnection, -}; +use serde_json::{json, Map, Value}; +use service_utils::service::types::DbConnection; +use service_utils::{bad_argument, db_error, unexpected_error}; + +use service_utils::result as superposition; pub fn endpoints() -> Scope { Scope::new("") @@ -35,7 +34,7 @@ pub fn endpoints() -> Scope { fn add_last_modified_header( max_created_at: Option<NaiveDateTime>, mut res: HttpResponse, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let header_name = HeaderName::from_static("last-modified"); if let Some(ele) = max_created_at { @@ -58,10 +57,7 @@ fn get_max_created_at( .and_then(|res| res.ok_or(diesel::result::Error::NotFound)) } -fn is_not_modified( - max_created_at: Option<NaiveDateTime>, - req: &HttpRequest, -) -> anyhow::Result<bool> { +fn is_not_modified(max_created_at: Option<NaiveDateTime>, req: &HttpRequest) -> bool { let nanosecond_erasure = |t: NaiveDateTime| t.with_nanosecond(0); let last_modified = req .headers() @@ -75,17 +71,20 @@ fn is_not_modified( .and_then(nanosecond_erasure); log::info!("last modified {last_modified:?}"); let parsed_max: Option<NaiveDateTime> = max_created_at.and_then(nanosecond_erasure); - Ok(max_created_at.is_some() && parsed_max <= last_modified) + max_created_at.is_some() && parsed_max <= last_modified } async fn generate_cac( conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> actix_web::Result<Config> { +) -> superposition::Result<Config> { let contexts_vec = ctxt::contexts .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) .order_by((ctxt::priority.asc(), ctxt::created_at.asc())) .load::<(String, Value, String, Value)>(conn) - .map_err_to_internal_server("error getting contexts", Null)?; + .map_err(|err| { + log::error!("failed to fetch contexts with error: {}", err); + db_error!(err) + })?; let (contexts, overrides) = contexts_vec.into_iter().fold( (Vec::new(), Map::new()), @@ -104,7 +103,10 @@ async fn generate_cac( let default_config_vec = def_conf::default_configs .select((def_conf::key, def_conf::value)) .load::<(String, Value)>(conn) - .map_err_to_internal_server("error getting default configs", Null)?; + .map_err(|err| { + log::error!("failed to fetch default_configs with error: {}", err); + db_error!(err) + })?; let default_configs = default_config_vec @@ -122,7 +124,10 @@ async fn generate_cac( } #[get("")] -async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpResponse> { +async fn get( + req: HttpRequest, + db_conn: DbConnection, +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let max_created_at = get_max_created_at(&mut conn) @@ -131,15 +136,17 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR log::info!("Max created at: {max_created_at:?}"); - let is_not_modified = is_not_modified(max_created_at, &req) - .map_err(|e| log::error!("config not modified: {e}")); + let is_not_modified = is_not_modified(max_created_at, &req); - if let Ok(true) = is_not_modified { + if is_not_modified { return Ok(HttpResponse::NotModified().finish()); } let params = Query::<HashMap<String, String>>::from_query(req.query_string()) - .map_err(|_| ErrorBadRequest("Unable to retrieve query parameters."))?; + .map_err(|err| { + log::error!("Failed to parse query params with err: {}", err); + bad_argument!("Unable to retrieve query parameters.") + })?; let mut query_params_map: serde_json::Map<String, Value> = Map::new(); for (key, value) in params.0.into_iter() { @@ -157,7 +164,7 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR .as_str() .ok_or_else(|| { log::error!("Prefix is not a valid string."); - err::InternalServerErr("Prefix is not a valid string.".to_string()) + bad_argument!("Prefix is not a valid string") })? .split(",") .collect(); @@ -177,10 +184,13 @@ async fn get(req: HttpRequest, db_conn: DbConnection) -> actix_web::Result<HttpR async fn get_resolved_config( req: HttpRequest, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let params = Query::<HashMap<String, String>>::from_query(req.query_string()) - .map_err(|_| ErrorBadRequest("error getting query params"))?; + .map_err(|err| { + log::error!("failed to parse query params with err: {}", err); + bad_argument!("error getting query params") + })?; let mut query_params_map: serde_json::Map<String, Value> = Map::new(); @@ -197,10 +207,9 @@ async fn get_resolved_config( .map_err(|e| log::error!("failed to fetch max timestamp from event_log : {e}")) .ok(); - let is_not_modified = is_not_modified(max_created_at, &req) - .map_err(|e| log::error!("config not modified: {e}")); + let is_not_modified = is_not_modified(max_created_at, &req); - if let Ok(true) = is_not_modified { + if is_not_modified { return Ok(HttpResponse::NotModified().finish()); } @@ -232,7 +241,10 @@ async fn get_resolved_config( &query_params_map, merge_strategy, ) - .map_err_to_internal_server("cac eval failed", Null)?, + .map_err(|err| { + log::error!("failed to eval cac with err: {}", err); + unexpected_error!("cac eval failed") + })?, ) } else { HttpResponse::Ok().json( @@ -243,7 +255,10 @@ async fn get_resolved_config( &query_params_map, merge_strategy, ) - .map_err_to_internal_server("cac eval failed", Null)?, + .map_err(|err| { + log::error!("failed to eval cac with err: {}", err); + unexpected_error!("cac eval failed") + })?, ) }; add_last_modified_header(max_created_at, response) @@ -253,10 +268,13 @@ async fn get_resolved_config( async fn get_filtered_config( req: HttpRequest, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let params = Query::<HashMap<String, String>>::from_query(req.query_string()) - .map_err(|_| ErrorBadRequest("error getting query params"))?; + .map_err(|err| { + log::error!("failed to parse query params with err: {}", err); + bad_argument!("Error getting query params.") + })?; let mut query_params_map: serde_json::Map<String, Value> = Map::new(); for (key, value) in params.0.into_iter() { @@ -279,9 +297,10 @@ async fn get_filtered_config( config .overrides .get(override_with_key) - .ok_or(err::InternalServerErr( - "Could not fetch override_key".to_string(), - ))? + .ok_or_else(|| { + log::error!("Could not fetch override_with_key"); + unexpected_error!("Something went wrong") + })? .to_owned(), ); } diff --git a/crates/context-aware-config/src/api/config/helpers.rs b/crates/context-aware-config/src/api/config/helpers.rs index 8b1d850bc..9b3f7f35b 100644 --- a/crates/context-aware-config/src/api/config/helpers.rs +++ b/crates/context-aware-config/src/api/config/helpers.rs @@ -4,13 +4,13 @@ use super::types::{Config, Context}; use serde_json::{Map, Value}; use service_utils::{ - errors::types::Error as err, helpers::extract_dimensions, types as app, + helpers::extract_dimensions, result as superposition, unexpected_error, }; pub fn filter_context( contexts: &Vec<Context>, query_params_map: &Map<String, Value>, -) -> app::Result<Vec<Context>> { +) -> superposition::Result<Vec<Context>> { let mut filtered_context: Vec<Context> = Vec::new(); for context in contexts.iter() { if should_add_ctx(&context, query_params_map)? { @@ -23,7 +23,7 @@ pub fn filter_context( fn should_add_ctx( context: &Context, query_params_map: &Map<String, Value>, -) -> app::Result<bool> { +) -> superposition::Result<bool> { let dimension = extract_dimensions(&context.condition)?; Ok(dimension .iter() @@ -33,7 +33,7 @@ fn should_add_ctx( pub fn filter_config_by_prefix( config: &Config, prefix_list: &HashSet<&str>, -) -> actix_web::Result<Config> { +) -> superposition::Result<Config> { let mut filtered_overrides: Map<String, Value> = Map::new(); let filtered_default_config: Map<String, Value> = config @@ -52,7 +52,7 @@ pub fn filter_config_by_prefix( .as_object() .ok_or_else(|| { log::error!("failed to decode overrides."); - err::InternalServerErr("failed to decode overrides.".to_string()) + unexpected_error!("failed to decode overrides.") })? .clone(); @@ -85,7 +85,7 @@ pub fn filter_config_by_prefix( pub fn filter_config_by_dimensions( config: &Config, query_params_map: &Map<String, Value>, -) -> actix_web::Result<Config> { +) -> superposition::Result<Config> { let filtered_context = filter_context(&config.contexts, &query_params_map)?; let filtered_overrides: Map<String, Value> = filtered_context .iter() diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context-aware-config/src/api/context/handlers.rs index 3cfec2d4e..3a0dbfc0b 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context-aware-config/src/api/context/handlers.rs @@ -6,7 +6,7 @@ use crate::{ api::{ context::types::{ ContextAction, ContextBulkResponse, DimensionCondition, MoveReq, - PaginationParams, PutReq, PutResp, TransactionError, + PaginationParams, PutReq, PutResp, }, dimension::get_all_dimension_schema_map, }, @@ -19,30 +19,30 @@ use crate::{ }, }; use actix_web::{ - delete, - error::{self, ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, - get, put, - web::{self, Path}, - HttpResponse, Responder, Result, Scope, + delete, get, put, + web::{Json, Path, Query}, + HttpResponse, Responder, Scope, }; -use anyhow::anyhow; use chrono::Utc; use dashboard_auth::types::User; use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, result::{DatabaseErrorKind::*, Error::DatabaseError}, - Connection, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, + Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{from_value, json, Map, Value}; -use service_utils::{helpers::ToActixErr, service::types::DbConnection}; +use service_utils::service::types::DbConnection; +use service_utils::{db_error, not_found, unexpected_error, validation_error}; use std::collections::HashMap; use super::helpers::{ validate_condition_with_functions, validate_override_with_functions, }; +use service_utils::{bad_argument, result as superposition}; + pub fn endpoints() -> Scope { Scope::new("") .service(put_handler) @@ -59,17 +59,18 @@ fn validate_dimensions_and_calculate_priority( object_key: &str, cond: &Value, dimension_schema_map: &HashMap<String, (JSONSchema, i32)>, -) -> Result<i32, String> { - let get_priority = |key: &String, val: &Value| -> Result<i32, String> { +) -> superposition::Result<i32> { + let get_priority = |key: &String, val: &Value| -> superposition::Result<i32> { if key == "var" { let dimension_name = val .as_str() - .ok_or_else(|| "Dimension name should be of String type")?; + .ok_or(bad_argument!("Dimension name should be of `String` type"))?; dimension_schema_map .get(dimension_name) .map(|(_, priority)| priority) - .ok_or(String::from( - "No matching `dimension` found in dimension table", + .ok_or(bad_argument!( + "No matching dimension ({}) found", + dimension_name )) .copied() } else { @@ -104,7 +105,10 @@ fn validate_dimensions_and_calculate_priority( let expected_dimension_name = dimension_condition.var; let (dimension_value_schema, _) = dimension_schema_map .get(&expected_dimension_name) - .ok_or(format!("No matching `dimension` {expected_dimension_name} in dimension table").as_str())?; + .ok_or(bad_argument!( + "No matching `dimension` {} in dimension table", + expected_dimension_name + ))?; validate_context_jsonschema( object_key, @@ -128,7 +132,7 @@ fn validate_dimensions_and_calculate_priority( fn validate_override_with_default_configs( conn: &mut DBConnection, override_: &Map<String, Value>, -) -> anyhow::Result<()> { +) -> superposition::Result<()> { let keys_array: Vec<&String> = override_.keys().collect(); let res: Vec<(String, Value)> = dsl::default_configs .filter(dsl::key.eq_any(keys_array)) @@ -141,9 +145,7 @@ fn validate_override_with_default_configs( let schema = map .get(key) // .map(|resp| resp) - .ok_or(anyhow!(format!( - "failed to compile json schema for key {key}" - )))?; + .ok_or(bad_argument!("failed to get schema for config key {}", key))?; let instance = value; let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) @@ -152,16 +154,19 @@ fn validate_override_with_default_configs( Ok(jschema) => jschema, Err(e) => { log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); - return Err(anyhow!("message: bad json schema")); + return Err(bad_argument!( + "failed to compile ({}) config key schema", + key + )); } }; if let Err(e) = jschema.validate(instance) { let verrors = e.collect::<Vec<ValidationError>>(); - log::error!("{:?}", verrors); - return Err(anyhow!(json!(format!( - "Schema validation failed for {key} with error {:?}", + log::error!("({key}) config key validation error: {:?}", verrors); + return Err(validation_error!( + "schema validation failed for {key} with error {:?}", verrors - )))); + )); }; } @@ -169,10 +174,10 @@ fn validate_override_with_default_configs( } fn create_ctx_from_put_req( - req: web::Json<PutReq>, + req: Json<PutReq>, conn: &mut DBConnection, user: &User, -) -> anyhow::Result<Context> { +) -> superposition::Result<Context> { let ctx_condition = Value::Object(req.context.to_owned()); let ctx_override: Value = req.r#override.to_owned().into(); validate_override_with_default_configs(conn, &req.r#override)?; @@ -181,19 +186,16 @@ fn create_ctx_from_put_req( let dimension_schema_map = get_all_dimension_schema_map(conn)?; - let priority = match validate_dimensions_and_calculate_priority( + let priority = validate_dimensions_and_calculate_priority( "context", &ctx_condition, &dimension_schema_map, - ) { - Ok(0) => { - return Err(anyhow!("No dimension found in context")); - } - Err(e) => { - return Err(anyhow!(e)); - } - Ok(p) => p, - }; + )?; + + if priority == 0 { + return Err(bad_argument!("No dimension found in context")); + } + let context_id = hash(&ctx_condition); let override_id = hash(&ctx_override); Ok(Context { @@ -215,7 +217,7 @@ fn hash(val: &Value) -> String { fn update_override_of_existing_ctx( conn: &mut PgConnection, ctx: Context, -) -> anyhow::Result<PutResp> { +) -> superposition::Result<PutResp> { use contexts::dsl; let mut new_override: Value = dsl::contexts .filter(dsl::id.eq(&ctx.id)) @@ -244,11 +246,11 @@ fn get_put_resp(ctx: Context) -> PutResp { } fn put( - req: web::Json<PutReq>, + req: Json<PutReq>, user: &User, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, already_under_txn: bool, -) -> anyhow::Result<PutResp> { +) -> superposition::Result<PutResp> { use contexts::dsl::contexts; let new_ctx = create_ctx_from_put_req(req, conn, user)?; @@ -267,72 +269,47 @@ fn put( update_override_of_existing_ctx(conn, new_ctx) } Err(e) => { - log::error!("update query failed with error: {e:?}"); - Err(anyhow!(e)) + log::error!("failed to update context with db error: {:?}", e); + Err(db_error!(e)) } } } #[put("")] async fn put_handler( - req: web::Json<PutReq>, + req: Json<PutReq>, user: User, mut db_conn: DbConnection, -) -> actix_web::Result<web::Json<PutResp>> { +) -> superposition::Result<Json<PutResp>> { put(req, &user, &mut db_conn, false) - .map(|resp| web::Json(resp)) - .map_err(|e: anyhow::Error| { - log::info!("context put failed with error: {:?}", e); - if let Some(io_error) = e.downcast_ref::<std::io::Error>() { - log::info!("{}", { io_error }); - ErrorInternalServerError("") - } else if ["Bad schema", "validation failed"] - .iter() - .any(|&s| e.to_string().contains(s)) - { - ErrorBadRequest(format!("{e}")) - } else { - ErrorInternalServerError("") - } - //TODO: Check why this is not working - // match e.downcast_ref::<std::io::Error>() { - // Some(err) => { - // if err.to_string().contains("Bad schema") { - // ErrorBadRequest("Schema Validation Failed") - // } else { - // ErrorInternalServerError("") - // } - // } - // None => ErrorInternalServerError(""), - // } + .map(|resp| Json(resp)) + .map_err(|err: superposition::AppError| { + log::info!("context put failed with error: {:?}", err); + err }) } fn r#move( old_ctx_id: String, - req: web::Json<MoveReq>, + req: Json<MoveReq>, user: &User, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, already_under_txn: bool, -) -> anyhow::Result<PutResp> { +) -> superposition::Result<PutResp> { use contexts::dsl; let req = req.into_inner(); let ctx_condition = Value::Object(req.context); let new_ctx_id = hash(&ctx_condition); let dimension_schema_map = get_all_dimension_schema_map(conn)?; - let priority = match validate_dimensions_and_calculate_priority( + let priority = validate_dimensions_and_calculate_priority( "context", &ctx_condition, &dimension_schema_map, - ) { - Ok(0) => { - return Err(anyhow!(String::from("No dimension found in context"))); - } - Err(e) => { - return Err(anyhow!(e)); - } - Ok(p) => p, - }; + )?; + + if priority == 0 { + return Err(bad_argument!("no dimension found in context")); + } if already_under_txn { diesel::sql_query("SAVEPOINT update_ctx_savepoint").execute(conn)?; @@ -386,8 +363,8 @@ fn r#move( handle_unique_violation(conn, already_under_txn) } Err(e) => { - log::error!("update query failed with error: {e:?}"); - Err(anyhow!(e)) + log::error!("failed to move context with db error: {:?}", e); + Err(db_error!(e)) } } } @@ -395,50 +372,40 @@ fn r#move( #[put("/move/{ctx_id}")] async fn move_handler( path: Path<String>, - req: web::Json<MoveReq>, + req: Json<MoveReq>, user: User, mut db_conn: DbConnection, -) -> actix_web::Result<web::Json<PutResp>> { +) -> superposition::Result<Json<PutResp>> { r#move(path.into_inner(), req, &user, &mut db_conn, false) - .map(|resp| web::Json(resp)) - .map_err(|e| { - log::info!("move api failed with error: {:?}", e); - ErrorInternalServerError("") + .map(|resp| Json(resp)) + .map_err(|err| { + log::info!("move api failed with error: {:?}", err); + err }) } #[get("/{ctx_id}")] async fn get_context( - path: web::Path<String>, + path: Path<String>, db_conn: DbConnection, -) -> Result<impl Responder> { +) -> superposition::Result<impl Responder> { use crate::db::schema::contexts::dsl::*; let ctx_id = path.into_inner(); let DbConnection(mut conn) = db_conn; - let result: QueryResult<Vec<Context>> = - contexts.filter(id.eq(ctx_id)).load(&mut conn); + let ctx: Context = contexts + .filter(id.eq(ctx_id)) + .get_result::<Context>(&mut conn)?; - let ctx_vec = match result { - Ok(ctx_vec) => ctx_vec, - Err(e) => { - log::info!("Failed to execute query, error: {e}"); - return Err(error::ErrorInternalServerError("")); - } - }; - - match ctx_vec.first() { - Some(ctx) => Ok(web::Json(ctx.clone())), - _ => Err(error::ErrorNotFound("")), - } + Ok(Json(ctx)) } #[get("/list")] async fn list_contexts( - qparams: web::Query<PaginationParams>, + qparams: Query<PaginationParams>, db_conn: DbConnection, -) -> Result<impl Responder> { +) -> superposition::Result<impl Responder> { use crate::db::schema::contexts::dsl::*; let DbConnection(mut conn) = db_conn; @@ -452,18 +419,18 @@ async fn list_contexts( let size = opt_size.unwrap_or(default_size); if page < 1 { - return Err(error::ErrorBadRequest("Param 'page' has to be at least 1.")); + return Err(bad_argument!("Param 'page' has to be at least 1.")); } else if size < 1 { - return Err(error::ErrorBadRequest("Param 'size' has to be at least 1.")); + return Err(bad_argument!("Param 'size' has to be at least 1.")); } let result: Vec<Context> = contexts .order(created_at) .limit(i64::from(size)) .offset(i64::from(size * (page - 1))) - .load(&mut conn) - .map_err_to_internal_server("Failed to execute query, error", Value::Null)?; - Ok(web::Json(result)) + .load(&mut conn)?; + + Ok(Json(result)) } #[delete("/{ctx_id}")] @@ -471,7 +438,7 @@ async fn delete_context( path: Path<String>, user: User, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { use contexts::dsl; let DbConnection(mut conn) = db_conn; @@ -479,50 +446,41 @@ async fn delete_context( let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(&mut conn); match deleted_row { - Ok(0) => Err(ErrorNotFound("")), + Ok(0) => Err(not_found!("Context Id `{}` doesn't exists", ctx_id)), Ok(_) => { log::info!("{ctx_id} context deleted by {}", user.email); Ok(HttpResponse::NoContent().finish()) } Err(e) => { log::error!("context delete query failed with error: {e}"); - Err(ErrorInternalServerError("")) + Err(unexpected_error!("Something went wrong.")) } } } #[put("/bulk-operations")] async fn bulk_operations( - reqs: web::Json<Vec<ContextAction>>, + reqs: Json<Vec<ContextAction>>, user: User, db_conn: DbConnection, -) -> actix_web::Result<web::Json<Vec<ContextBulkResponse>>> { +) -> superposition::Result<Json<Vec<ContextBulkResponse>>> { use contexts::dsl::contexts; let DbConnection(mut conn) = db_conn; - let mut resp = Vec::<ContextBulkResponse>::new(); - let result = conn.transaction::<_, TransactionError, _>(|transaction_conn| { + let mut response = Vec::<ContextBulkResponse>::new(); + conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { for action in reqs.into_inner().into_iter() { match action { ContextAction::PUT(put_req) => { - let resp_result = - put(actix_web::web::Json(put_req), &user, transaction_conn, true); - - match resp_result { - Ok(put_resp) => { - resp.push(ContextBulkResponse::PUT(put_resp)); - } - Err(e) => { - log::error!("Failed at insert into contexts due to {:?}", e); - if e.to_string().contains("Bad schema") { - return Err(TransactionError::BadRequest(e.to_string())); - } else { - return Err(TransactionError::DieselError( - diesel::result::Error::RollbackTransaction, - )); - } - } - } + let put_resp = put(Json(put_req), &user, transaction_conn, true) + .map_err(|err| { + log::error!( + "Failed at insert into contexts due to {:?}", + err + ); + err + })?; + response.push(ContextBulkResponse::PUT(put_resp)); } ContextAction::DELETE(ctx_id) => { let deleted_row = @@ -531,53 +489,38 @@ async fn bulk_operations( match deleted_row { // Any kind of error would rollback the tranction but explicitly returning rollback tranction allows you to rollback from any point in transaction. Ok(0) => { - return Err(TransactionError::DieselError( - diesel::result::Error::RollbackTransaction, + return Err(bad_argument!( + "context with id {} not found", + ctx_id )) } Ok(_) => { log::info!("{ctx_id} context deleted by {email}"); - resp.push(ContextBulkResponse::DELETE(format!( + response.push(ContextBulkResponse::DELETE(format!( "{ctx_id} deleted succesfully" ))) } Err(e) => { log::error!("Delete context failed due to {:?}", e); - return Err(TransactionError::DieselError( - diesel::result::Error::RollbackTransaction, - )); + return Err(db_error!(e)); } }; } ContextAction::MOVE((old_ctx_id, move_req)) => { - let move_context_resp = r#move( - old_ctx_id, - actix_web::web::Json(move_req), - &user, - transaction_conn, - true, - ); - - match move_context_resp { - Ok(move_resp) => resp.push(ContextBulkResponse::MOVE(move_resp)), - Err(e) => { - log::error!( - "Failed at moving context reponse due to {:?}", - e - ); - return Err(TransactionError::DieselError( - diesel::result::Error::RollbackTransaction, - )); - } - }; + let move_context_resp = + r#move(old_ctx_id, Json(move_req), &user, transaction_conn, true) + .map_err(|err| { + log::error!( + "Failed at moving context reponse due to {:?}", + err + ); + err + })?; + response.push(ContextBulkResponse::MOVE(move_context_resp)); } } } Ok(()) // Commit the transaction - }); - match result { - Ok(_) => Ok(web::Json(resp)), - Err(TransactionError::BadRequest(_)) => Err(ErrorBadRequest("")), - Err(TransactionError::DieselError(_)) => Err(ErrorInternalServerError("")), - } + })?; + Ok(Json(response)) } diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context-aware-config/src/api/context/helpers.rs index 60b010202..caae40a77 100644 --- a/crates/context-aware-config/src/api/context/helpers.rs +++ b/crates/context-aware-config/src/api/context/helpers.rs @@ -1,6 +1,7 @@ extern crate base64; use base64::prelude::*; use service_utils::helpers::extract_dimensions; +use service_utils::{result as superposition, unexpected_error, validation_error}; use std::str; use crate::api::functions::helpers::get_published_functions_by_names; @@ -12,19 +13,18 @@ use crate::{ dimensions::{self}, }, }; -use anyhow::anyhow; use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use serde_json::{json, Map, Value}; +use serde_json::{Map, Value}; use std::collections::HashMap; type DBConnection = PooledConnection<ConnectionManager<PgConnection>>; pub fn validate_condition_with_functions( conn: &mut DBConnection, context: &Value, -) -> anyhow::Result<()> { +) -> superposition::Result<()> { use dimensions::dsl; let context = extract_dimensions(&context)?; let dimensions_list: Vec<String> = context.keys().cloned().collect(); @@ -53,7 +53,7 @@ pub fn validate_condition_with_functions( pub fn validate_override_with_functions( conn: &mut DBConnection, override_: &Map<String, Value>, -) -> anyhow::Result<()> { +) -> superposition::Result<()> { let default_config_keys: Vec<String> = override_.keys().cloned().collect(); let keys_function_array: Vec<(String, Option<String>)> = dsl::default_configs .filter(dsl::key.eq_any(default_config_keys)) @@ -80,7 +80,7 @@ pub fn validate_override_with_functions( fn get_functions_map( conn: &mut DBConnection, keys_function_array: Vec<(String, String)>, -) -> anyhow::Result<HashMap<String, FunctionsInfo>> { +) -> superposition::Result<HashMap<String, FunctionsInfo>> { let functions_map: HashMap<String, Option<String>> = get_published_functions_by_names( conn, @@ -108,19 +108,29 @@ fn get_functions_map( Ok(default_config_functions_map) } -fn validate_value_with_function( +pub fn validate_value_with_function( fun_name: &str, function: &str, key: &String, value: &Value, -) -> anyhow::Result<()> { - let base64_decoded = BASE64_STANDARD.decode(function)?; - let utf8_decoded = str::from_utf8(&base64_decoded)?; - if let Err((e, stdout)) = execute_fn(&utf8_decoded, key, value.to_owned()) { - log::error!("function validation failed for {key} with error: {e}"); - return Err(anyhow!(json!({ - "message": format!("function validation failed for {} with error {}", key, e), "stdout": stdout - }))); +) -> superposition::Result<()> { + let base64_decoded = BASE64_STANDARD.decode(function).map_err(|err| { + log::error!("Failed to decode function code: {}", err); + unexpected_error!("Failed to decode function code: {}", err) + })?; + let utf8_decoded = str::from_utf8(&base64_decoded).map_err(|err| { + log::error!("Failed to parse function code in UTF-8: {}", err); + unexpected_error!("Failed to parse function code in UTF-8: {}", err) + })?; + if let Err((err, stdout)) = execute_fn(&utf8_decoded, key, value.to_owned()) { + let stdout = stdout.unwrap_or(String::new()); + log::error!("function validation failed for {key} with error: {err}"); + return Err(validation_error!( + "Function validation failed for {} with error {}. {}", + key, + err, + stdout + )); } Ok(()) } diff --git a/crates/context-aware-config/src/api/context/mod.rs b/crates/context-aware-config/src/api/context/mod.rs index fa13772fb..8c255ad45 100644 --- a/crates/context-aware-config/src/api/context/mod.rs +++ b/crates/context-aware-config/src/api/context/mod.rs @@ -1,4 +1,4 @@ mod handlers; -mod helpers; +pub mod helpers; mod types; pub use handlers::endpoints; diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context-aware-config/src/api/context/types.rs index f8a3d5184..c376fbd15 100644 --- a/crates/context-aware-config/src/api/context/types.rs +++ b/crates/context-aware-config/src/api/context/types.rs @@ -44,18 +44,6 @@ pub enum ContextBulkResponse { MOVE(PutResp), } -#[derive(Debug)] -pub enum TransactionError { - DieselError(diesel::result::Error), - BadRequest(String), // Custom error type for bad requests -} - -impl From<diesel::result::Error> for TransactionError { - fn from(error: diesel::result::Error) -> Self { - TransactionError::DieselError(error) - } -} - #[derive(Deserialize, Clone)] pub struct FunctionsInfo { pub name: String, diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context-aware-config/src/api/default_config/handlers.rs index eb023281d..ed264c624 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context-aware-config/src/api/default_config/handlers.rs @@ -1,16 +1,14 @@ extern crate base64; use super::types::CreateReq; -use base64::prelude::*; -use std::str; +use service_utils::{bad_argument, unexpected_error, validation_error}; -use crate::validation_functions::execute_fn; +use crate::api::context::helpers::validate_value_with_function; use crate::{ api::functions::helpers::get_published_function_code, db::{self, models::DefaultConfig, schema::default_configs::dsl::default_configs}, helpers::validate_jsonschema, }; use actix_web::{ - error::{ErrorBadRequest, ErrorInternalServerError}, get, put, web::{self, Data, Json}, HttpResponse, Scope, @@ -24,8 +22,8 @@ use diesel::{ use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; use service_utils::{ + result as superposition, service::types::{AppState, DbConnection}, - types as app, }; pub fn endpoints() -> Scope { @@ -39,25 +37,23 @@ async fn create( request: web::Json<CreateReq>, user: User, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); let key = key.into_inner(); if req.value.is_none() && req.schema.is_none() && req.function_name.is_none() { log::error!("No data provided in the request body for {key}"); - return Err(ErrorBadRequest(json!({ - "message": "Please provide data in the request body." - }))); + return Err(bad_argument!("Please provide data in the request body.")); } let func_name = match &req.function_name { Some(Value::String(s)) => Some(s.clone()), Some(Value::Null) | None => None, Some(_) => { - return Err(ErrorBadRequest(json!({ - "message": "Expected a string or null as the function name." - }))) + return Err(bad_argument!( + "Expected a string or null as the function name.", + )) } }; @@ -74,20 +70,18 @@ async fn create( }; (val, schema, f_name) } - Err(diesel::NotFound) => match (req.value, req.schema) { - (Some(val), Some(schema)) => (val, Value::Object(schema), func_name), - _ => { - log::error!("No record found for {key}."); - return Err(ErrorBadRequest(json!({ - "message": format!( "No record found for {key}." ) - }))); + Err(superposition::AppError::DbError(diesel::NotFound)) => { + match (req.value, req.schema) { + (Some(val), Some(schema)) => (val, Value::Object(schema), func_name), + _ => { + log::error!("No record found for {key}."); + return Err(bad_argument!("No record found for {}", key)); + } } - }, + } Err(e) => { log::error!("Failed to fetch default_config {key} with error: {e}."); - return Err(ErrorBadRequest(json!({ - "message": format!( "Something went wrong." ) - }))); + return Err(unexpected_error!("Something went wrong.")); } }; @@ -100,12 +94,11 @@ async fn create( created_at: Utc::now(), }; - if let Err(e) = validate_jsonschema( + validate_jsonschema( &state.default_config_validation_schema, &default_config.schema, - ) { - return Err(ErrorBadRequest(json!({ "message": e }))); - }; + )?; + let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) .compile(&default_config.schema); @@ -113,7 +106,7 @@ async fn create( Ok(jschema) => jschema, Err(e) => { log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); - return Err(ErrorBadRequest(json!({"message": "Bad json schema."}))); + return Err(bad_argument!("Invalid JSON schema (failed to compile)")); } }; @@ -123,8 +116,10 @@ async fn create( "Validation for value with given JSON schema failed: {:?}", verrors ); - return Err(ErrorBadRequest( - json!({"message": "Validation with given schema failed."}), + return Err(validation_error!( + "Schema validation failed for key {} with error {:?}", + default_config.key, + verrors )); } @@ -132,24 +127,15 @@ async fn create( let function_code = get_published_function_code(&mut conn, f_name.to_string()) .map_err(|e| { log::info!("Function not found with error : {e}"); - ErrorBadRequest(json!({"message" : "Function not found."})) + bad_argument!("Function {} doesn't exists.", f_name) })?; if let Some(f_code) = function_code { - let base64_decoded = BASE64_STANDARD.decode(f_code.clone()).map_err(|e| { - ErrorInternalServerError(json!({"message": e.to_string()})) - })?; - let utf8_decoded = str::from_utf8(&base64_decoded).map_err(|e| { - ErrorInternalServerError(json!({"message": e.to_string()})) - })?; - - if let Err((e, stdout)) = - execute_fn(&utf8_decoded, &key, default_config.value.to_owned()) - { - log::info!("function validation failed for {key} with error: {e}"); - return Err(ErrorBadRequest(json!({ - "message": format!( "function validation failed with error: {}", e), "stdout": stdout - }))); - } + validate_value_with_function( + f_name, + &f_code, + &default_config.key, + &default_config.value, + )?; } } @@ -166,8 +152,8 @@ async fn create( }))), Err(e) => { log::info!("DefaultConfig creation failed with error: {e}"); - Err(ErrorInternalServerError( - json!({"message": "Failed to create DefaultConfig"}), + Err(unexpected_error!( + "Something went wrong, failed to create DefaultConfig" )) } } @@ -176,7 +162,7 @@ async fn create( fn fetch_default_key( key: &String, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> Result<(Value, Value, Option<String>), diesel::result::Error> { +) -> superposition::Result<(Value, Value, Option<String>)> { let res: (Value, Value, Option<String>) = default_configs .filter(db::schema::default_configs::key.eq(key)) .select(( @@ -189,7 +175,7 @@ fn fetch_default_key( } #[get("")] -async fn get(db_conn: DbConnection) -> app::Result<Json<Vec<DefaultConfig>>> { +async fn get(db_conn: DbConnection) -> superposition::Result<Json<Vec<DefaultConfig>>> { let DbConnection(mut conn) = db_conn; let result: Vec<DefaultConfig> = default_configs.get_results(&mut conn)?; diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context-aware-config/src/api/dimension/handlers.rs index fc86716e4..859e4a3a9 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context-aware-config/src/api/dimension/handlers.rs @@ -14,8 +14,9 @@ use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; use service_utils::{ + bad_argument, result as superposition, service::types::{AppState, DbConnection}, - types as app, + unexpected_error, }; pub fn endpoints() -> Scope { @@ -28,28 +29,28 @@ async fn create( req: web::Json<CreateReq>, user: User, db_conn: DbConnection, -) -> HttpResponse { +) -> superposition::Result<HttpResponse> { //TODO move this to the type itself rather than special if check let DbConnection(mut conn) = db_conn; if req.priority <= 0 { - return HttpResponse::BadRequest().body("Priority should be greater than 0"); + return Err(bad_argument!("Priority should be greater than 0")); } let create_req = req.into_inner(); let schema_value = create_req.schema; - if let Err(e) = validate_jsonschema(&state.meta_schema, &schema_value) { - return HttpResponse::BadRequest().body(e); - }; + validate_jsonschema(&state.meta_schema, &schema_value)?; let schema_compile_result = JSONSchema::options() .with_draft(Draft::Draft7) .compile(&schema_value); if let Err(e) = schema_compile_result { - return HttpResponse::BadRequest() - .body(String::from(format!("Bad schema: {:?}", e))); + return Err(bad_argument!( + "Invalid JSON schema (failed to compile): {:?}", + e + )); }; let fun_name = match create_req.function_name { @@ -57,9 +58,9 @@ async fn create( Some(Value::Null) | None => None, _ => { log::error!("Expected a string or null as the function name."); - return HttpResponse::BadRequest().body(String::from(format!( + return Err(bad_argument!( "Expected a string or null as the function name." - ))); + )); } }; @@ -77,31 +78,33 @@ async fn create( .on_conflict(dimension) .do_update() .set(&new_dimension) - .execute(&mut conn); + .get_result::<Dimension>(&mut conn); match upsert { - Ok(_) => { - return HttpResponse::Created() - .body("Dimension created/updated successfully.") + Ok(upserted_dimension) => { + return Ok(HttpResponse::Created().json(upserted_dimension)) } Err(diesel::result::Error::DatabaseError( diesel::result::DatabaseErrorKind::ForeignKeyViolation, e, )) => { log::error!("{fun_name:?} function not found with error: {e:?}"); - return HttpResponse::BadRequest() - .body(String::from(format!("Function not found."))); + return Err(bad_argument!( + "Funtion {} doesn't exists", + fun_name.unwrap_or(String::new()) + )); } Err(e) => { log::error!("Dimension upsert failed with error: {e}"); - return HttpResponse::InternalServerError() - .body("Failed to create/update dimension\n"); + return Err(unexpected_error!( + "Something went wrong, failed to create/update dimension" + )); } } } #[get("")] -async fn get(db_conn: DbConnection) -> app::Result<Json<Vec<Dimension>>> { +async fn get(db_conn: DbConnection) -> superposition::Result<Json<Vec<Dimension>>> { let DbConnection(mut conn) = db_conn; let result: Vec<Dimension> = dimensions.get_results(&mut conn)?; diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context-aware-config/src/api/dimension/utils.rs index 8c24b25f7..07b6c9342 100644 --- a/crates/context-aware-config/src/api/dimension/utils.rs +++ b/crates/context-aware-config/src/api/dimension/utils.rs @@ -7,10 +7,11 @@ use diesel::{ PgConnection, }; use jsonschema::{Draft, JSONSchema}; +use service_utils::result as superposition; pub fn get_all_dimension_schema_map( conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> anyhow::Result<HashMap<String, (JSONSchema, i32)>> { +) -> superposition::Result<HashMap<String, (JSONSchema, i32)>> { let dimensions_vec = dimensions.load::<Dimension>(conn)?; let dimension_schema_map = dimensions_vec diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context-aware-config/src/api/functions/handlers.rs index b3b635c63..f045cc69e 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context-aware-config/src/api/functions/handlers.rs @@ -13,9 +13,7 @@ use crate::{ validation_functions, }; use actix_web::{ - delete, - error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}, - get, patch, post, put, + delete, get, patch, post, put, web::{self, Json, Path}, HttpResponse, Result, Scope, }; @@ -23,7 +21,8 @@ use chrono::Utc; use dashboard_auth::types::User; use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::json; -use service_utils::service::types::DbConnection; +use service_utils::{bad_argument, not_found, service::types::DbConnection}; +use service_utils::{result as superposition, unexpected_error}; use validation_functions::{compile_fn, execute_fn}; use super::types::{CreateFunctionRequest, UpdateFunctionRequest}; @@ -44,13 +43,11 @@ async fn create( user: User, request: web::Json<CreateFunctionRequest>, db_conn: DbConnection, -) -> Result<Json<Function>> { +) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - if let Err(e) = compile_fn(&req.function) { - return Err(ErrorBadRequest(json!({ "message": e }))); - } + compile_fn(&req.function)?; let function = Function { function_name: req.function_name, @@ -79,21 +76,19 @@ async fn create( log::error!("Function error: {:?}", e); match kind { diesel::result::DatabaseErrorKind::UniqueViolation => { - return Err(ErrorBadRequest( - json!({"message": "Function already exists."}), - )) + return Err(bad_argument!("Function already exists.")) } _ => { - return Err(ErrorBadRequest( - json!({"message": "An error occured please contact the admin"}), + return Err(unexpected_error!( + "Something went wrong, failed to create function" )) } } } _ => { log::error!("Function creation failed with error: {e}"); - return Err(ErrorInternalServerError( - json!({"message": "An error occured please contact the admin."}), + return Err(unexpected_error!( + "An error occured please contact the admin." )); } }, @@ -106,30 +101,26 @@ async fn update( params: web::Path<String>, request: web::Json<UpdateFunctionRequest>, db_conn: DbConnection, -) -> Result<Json<Function>> { +) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); let f_name = params.into_inner(); let result = match fetch_function(&f_name, &mut conn) { Ok(val) => val, - Err(diesel::result::Error::NotFound) => { + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { log::error!("Function not found."); - return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + return Err(bad_argument!("Function {} doesn't exists", f_name)); } Err(e) => { log::error!("Failed to update Function with error: {e}"); - return Err(ErrorInternalServerError( - json!({"message": "Failed to update Function"}), - )); + return Err(unexpected_error!("Failed to update Function")); } }; // Function Linter Check if let Some(function) = &req.function { - if let Err(e) = compile_fn(function) { - return Err(ErrorBadRequest(json!({ "message": e }))); - } + compile_fn(function)?; } let new_function = Function { @@ -150,65 +141,38 @@ async fn update( published_runtime_version: result.published_runtime_version, }; - let update: Result<Function, diesel::result::Error> = diesel::update(functions) + let mut updated_function = diesel::update(functions) .filter(db::schema::functions::function_name.eq(f_name)) .set(new_function) - .get_result(&mut conn); + .get_result::<Function>(&mut conn)?; - match update { - Ok(mut res) => { - decode_function(&mut res)?; - Ok(Json(res)) - } - Err(e) => { - log::error!("Function updation failed with error: {e}"); - Err(ErrorInternalServerError( - json!({"message": "Failed to update Function"}), - )) - } - } + decode_function(&mut updated_function)?; + Ok(Json(updated_function)) } #[get("/{function_name}")] -async fn get(params: web::Path<String>, db_conn: DbConnection) -> Result<HttpResponse> { +async fn get( + params: web::Path<String>, + db_conn: DbConnection, +) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let f_name = params.into_inner(); - let result = fetch_function(&f_name, &mut conn); + let mut function = fetch_function(&f_name, &mut conn)?; - match result { - Ok(mut function) => { - decode_function(&mut function)?; - Ok(HttpResponse::Ok().json(function)) - } - Err(e) => { - log::error!("Error getting function: {e}"); - Err(ErrorInternalServerError( - json!({"message": "Function does not exists."}), - )) - } - } + decode_function(&mut function)?; + Ok(Json(function)) } #[get("")] -async fn list_functions(db_conn: DbConnection) -> Result<Json<Vec<Function>>> { +async fn list_functions( + db_conn: DbConnection, +) -> superposition::Result<Json<Vec<Function>>> { let DbConnection(mut conn) = db_conn; - let result: Result<Vec<Function>, diesel::result::Error> = - functions.get_results(&mut conn); - - match result { - Ok(mut function_list) => { - for function in function_list.iter_mut() { - decode_function(function)?; - } - Ok(Json(function_list)) - } - Err(e) => { - log::error!("Error getting the functions: {e}"); - Err(ErrorInternalServerError( - json!({"message": "Error getting the functions."}), - )) - } + let mut function_list = functions.get_results(&mut conn)?; + for function in function_list.iter_mut() { + decode_function(function)?; } + Ok(Json(function_list)) } #[delete("/{function_name}")] @@ -216,21 +180,23 @@ async fn delete_function( user: User, params: web::Path<String>, db_conn: DbConnection, -) -> Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let f_name = params.into_inner(); let deleted_row = delete(functions.filter(function_name.eq(&f_name))).execute(&mut conn); match deleted_row { - Ok(0) => Err(ErrorNotFound(json!({"message": "Function not found."}))), + Ok(0) => Err(not_found!("Function {} doesn't exists", f_name)), Ok(_) => { log::info!("{f_name} function deleted by {}", user.email); Ok(HttpResponse::NoContent().finish()) } Err(e) => { log::error!("function delete query failed with error: {e}"); - Err(ErrorInternalServerError("")) + Err(unexpected_error!( + "Something went wrong, failed to delete the function" + )) } } } @@ -240,24 +206,25 @@ async fn test( params: Path<TestParam>, request: web::Json<TestFunctionRequest>, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let path_params = params.into_inner(); let fun_name = &path_params.function_name; let req = request.into_inner(); let mut function = match fetch_function(fun_name, &mut conn) { Ok(val) => val, - Err(diesel::result::Error::NotFound) => { + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { log::error!("Function not found."); - return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + return Err(bad_argument!("Function {} doesn't exists", fun_name)); } Err(e) => { - log::error!("Failed to update Function with error: {e}"); - return Err(ErrorInternalServerError( - json!({"message": "Failed to update Function due to unexpected DB issue"}), + log::error!("Failed to fetch Function {fun_name} with error: {e}"); + return Err(unexpected_error!( + "Something went wrong, failed to update function" )); } }; + decode_function(&mut function)?; let result = match path_params.stage { Stage::DRAFT => execute_fn(&function.draft_code, &req.key, req.value), @@ -276,7 +243,7 @@ async fn test( match result { Ok(stdout) => Ok(HttpResponse::Ok() .json(json!({"message": "Function validated the given value successfully", "stdout": stdout}))), - Err((e, stdout)) => Err(ErrorBadRequest(json!({ "message": format!( "Function validation failed with error: {e}" ), "stdout": stdout }))), + Err((e, stdout)) => Err(bad_argument!("Function validation failed with error: {}, stdout: {:?}", e, stdout )), } } @@ -285,45 +252,34 @@ async fn publish( user: User, params: web::Path<String>, db_conn: DbConnection, -) -> actix_web::Result<HttpResponse> { +) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let fun_name = params.into_inner(); let function = match fetch_function(&fun_name, &mut conn) { Ok(val) => val, - Err(diesel::result::Error::NotFound) => { - log::error!("Function not found."); - return Err(ErrorBadRequest(json!({"message": "Function not found."}))); + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { + log::error!("Function {} not found.", fun_name); + return Err(bad_argument!("Function {} doesn't exists", fun_name)); } Err(e) => { log::error!("Failed to update Function with error: {e}"); - return Err(ErrorInternalServerError( - json!({"message": "Failed to update Function"}), + return Err(unexpected_error!( + "Something went wrong, failed to update function" )); } }; - let updated_function: Result<Function, diesel::result::Error> = - diesel::update(functions) - .filter(dsl::function_name.eq(fun_name)) - .set(( - dsl::published_code.eq(Some(function.draft_code.clone())), - dsl::published_runtime_version - .eq(Some(function.draft_runtime_version.clone())), - dsl::published_by.eq(Some(user.email)), - dsl::published_at.eq(Some(Utc::now().naive_utc())), - )) - .get_result(&mut conn); + let updated_function = diesel::update(functions) + .filter(dsl::function_name.eq(fun_name.clone())) + .set(( + dsl::published_code.eq(Some(function.draft_code.clone())), + dsl::published_runtime_version + .eq(Some(function.draft_runtime_version.clone())), + dsl::published_by.eq(Some(user.email)), + dsl::published_at.eq(Some(Utc::now().naive_utc())), + )) + .get_result::<Function>(&mut conn)?; - match updated_function { - Ok(_) => Ok(HttpResponse::Ok().json(json!({ - "message": "Function published successfully." - }))), - Err(e) => { - log::error!("Function publish failed with error: {e}"); - Err(ErrorInternalServerError( - json!({"message": "Failed to publish Function due to unexpected DB issue"}), - )) - } - } + Ok(Json(updated_function)) } diff --git a/crates/context-aware-config/src/api/functions/helpers.rs b/crates/context-aware-config/src/api/functions/helpers.rs index 0ecc38dd0..643364f34 100644 --- a/crates/context-aware-config/src/api/functions/helpers.rs +++ b/crates/context-aware-config/src/api/functions/helpers.rs @@ -1,12 +1,10 @@ extern crate base64; -use actix_web::error::ErrorInternalServerError; use base64::prelude::*; use diesel::{ r2d2::{ConnectionManager, PooledConnection}, - result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use serde_json::json; +use service_utils::{result as superposition, unexpected_error}; use std::str; use crate::db::{self, models::Function, schema::functions::dsl::functions}; @@ -14,13 +12,13 @@ use crate::db::{self, models::Function, schema::functions::dsl::functions}; pub fn fetch_function( f_name: &String, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> actix_web::Result<Function, Error> { +) -> superposition::Result<Function> { Ok(functions .filter(db::schema::functions::function_name.eq(f_name)) .get_result::<Function>(conn)?) } -pub fn decode_function(function: &mut Function) -> actix_web::Result<()> { +pub fn decode_function(function: &mut Function) -> superposition::Result<()> { function.draft_code = decode_base64_to_string(&function.draft_code)?; if let Some(code) = &function.published_code { function.published_code = Some(decode_base64_to_string(&code)?); @@ -28,21 +26,19 @@ pub fn decode_function(function: &mut Function) -> actix_web::Result<()> { Ok(()) } -pub fn decode_base64_to_string(code: &String) -> actix_web::Result<String> { +pub fn decode_base64_to_string(code: &String) -> superposition::Result<String> { BASE64_STANDARD .decode(code) .map_err(|e| { log::info!("Error while decoding function: {}", e); - ErrorInternalServerError(json!({"message": "Failed to decode function"})) + unexpected_error!("Failed to decode function") }) .and_then(|decoded_code| { str::from_utf8(&decoded_code) .map(|res| res.to_string()) .map_err(|e| { log::info!("Error while decoding function: {}", e); - ErrorInternalServerError( - json!({"message": "Failed to decode function"}), - ) + unexpected_error!("Something went wrong, failed to decode function") }) }) } @@ -50,7 +46,7 @@ pub fn decode_base64_to_string(code: &String) -> actix_web::Result<String> { pub fn get_published_function_code( conn: &mut PooledConnection<ConnectionManager<PgConnection>>, f_name: String, -) -> anyhow::Result<Option<String>> { +) -> superposition::Result<Option<String>> { let function = functions .filter(db::schema::functions::function_name.eq(f_name)) .select(db::schema::functions::published_code) @@ -61,7 +57,7 @@ pub fn get_published_function_code( pub fn get_published_functions_by_names( conn: &mut PooledConnection<ConnectionManager<PgConnection>>, function_names: Vec<String>, -) -> anyhow::Result<Vec<(String, Option<String>)>> { +) -> superposition::Result<Vec<(String, Option<String>)>> { let function: Vec<(String, Option<String>)> = functions .filter(db::schema::functions::function_name.eq_any(function_names)) .select(( diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context-aware-config/src/helpers.rs index 6687ca349..8b0d943c8 100644 --- a/crates/context-aware-config/src/helpers.rs +++ b/crates/context-aware-config/src/helpers.rs @@ -2,6 +2,7 @@ use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; use itertools::{self, Itertools}; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; +use service_utils::{result as superposition, validation_error}; use std::collections::HashMap; pub fn get_default_config_validation_schema() -> JSONSchema { @@ -136,7 +137,7 @@ pub fn validate_context_jsonschema( object_key: &str, dimension_value: &Value, dimension_schema: &JSONSchema, -) -> Result<(), String> { +) -> superposition::Result<()> { match dimension_value { Value::Array(val_arr) if object_key == "in" => { let mut verrors = Vec::new(); @@ -166,15 +167,27 @@ pub fn validate_context_jsonschema( "Validation errors for dimensions in array: {:?}", verrors ); - Err(format!("Bad schema: {:?}", verrors)) + Err(validation_error!( + "failed to validate dimension value {:?} with error: {:?}", + dimension_value, + verrors + )) } } } } _ => dimension_schema.validate(dimension_value).map_err(|e| { let verrors = e.collect::<Vec<ValidationError>>(); - log::error!("Validation error: {:?}", verrors); - format!("Bad schema: {:?}", verrors) + log::error!( + "failed to validate dimension value {:?} with error : {:?}", + dimension_value, + verrors + ); + validation_error!( + "failed to validate dimension value {:?} with error: {:?}", + dimension_value, + verrors + ) }), } } @@ -189,16 +202,16 @@ pub fn validate_context_jsonschema( pub fn validate_jsonschema( validation_schema: &JSONSchema, schema: &Value, -) -> Result<(), String> { +) -> superposition::Result<()> { let res = match validation_schema.validate(schema) { Ok(_) => Ok(()), Err(e) => { //TODO: Try & render as json. let verrors = e.collect::<Vec<ValidationError>>(); - Err(String::from(format!( - "Bad schema: {:?}", + Err(validation_error!( + "schema validation failed: {:?}", verrors.as_slice() - ))) + )) } }; res @@ -356,15 +369,15 @@ mod tests { let err_arr_context = match validate_context_jsonschema("==", &arr_dimension_val, &test_jsonschema) { - Ok(_) => false, - Err(e) => { - print!("{:?}", e); - e.contains("Bad schema") + Err(superposition::AppError::ValidationError(err)) => { + log::info!("{:?}", err); + true } + _ => false, }; - assert_eq!(ok_str_context, Ok(())); + assert_eq!(ok_str_context.unwrap(), ()); assert_eq!(err_arr_context, true); - assert_eq!(ok_arr_context, Ok(())); + assert_eq!(ok_arr_context.unwrap(), ()); } } diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context-aware-config/src/validation_functions.rs index 291096588..3f0f7b417 100644 --- a/crates/context-aware-config/src/validation_functions.rs +++ b/crates/context-aware-config/src/validation_functions.rs @@ -1,4 +1,7 @@ use serde_json::{json, Value}; +use service_utils::result as superposition; +use service_utils::unexpected_error; +use service_utils::validation_error; use std::process::Command; use std::str; @@ -7,14 +10,14 @@ fn type_check_validate(code_str: &str) -> String { r#"const vm = require("node:vm") const axios = require("./target/node_modules/axios") const script = new vm.Script(\` - + {} if(typeof(validate)!="function") {{ throw Error("validate is not of function type") }}\`); - + script.runInNewContext({{axios,console}}, {{ timeout: 1500}}); "#, code_str @@ -27,10 +30,10 @@ fn execute_validate_fun(code_str: &str, val: Value) -> String { const vm = require("node:vm") const axios = require("./target/node_modules/axios") const script = new vm.Script(\` - + {} Promise.resolve(validate({})).then((output) => {{ - + if(output!=true){{ throw new Error("The function did not return true as expected. Check the conditions or logic inside the function.") }} @@ -38,7 +41,7 @@ fn execute_validate_fun(code_str: &str, val: Value) -> String { }}).catch((err)=> {{ throw new Error(err) }});\`); - + script.runInNewContext({{axios,console,process}}, {{ timeout: 1500}}); "#, code_str, val @@ -51,7 +54,7 @@ fn generate_code(code_str: &str) -> String { const {{ Worker, isMainThread, threadId }} = require("node:worker_threads"); if (isMainThread) {{ - + // starting worker thread , making separated from the main thread function runService() {{ return new Promise((resolve, reject) => {{ @@ -82,7 +85,7 @@ fn generate_code(code_str: &str) -> String { }} // terminate worker thread if execution time exceed 2 secs - + var tl = setTimeout(timelimit, 2000); }}); }} @@ -139,7 +142,7 @@ pub fn execute_fn( } } -pub fn compile_fn(code_str: &str) -> Result<(), String> { +pub fn compile_fn(code_str: &str) -> superposition::Result<()> { let type_check_code = type_check_validate(code_str); let output = Command::new("node") .arg("-e") @@ -154,14 +157,14 @@ pub fn compile_fn(code_str: &str) -> Result<(), String> { .unwrap_or("[Invalid UTF-8 in stderr]") .to_owned(); log::error!("{}", format!("eslint check output error: {:?}", stderr)); - Err(stderr) + Err(validation_error!(stderr)) } else { Ok(()) } } Err(e) => { log::error!("eslint check error: {}", e); - Err(format!("js_eval error: {}", e)) + Err(unexpected_error!("js_eval error: {}", e)) } } } diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context-aware-config/tests/cac_tests.rs index e9e0fa785..cd812a1dd 100644 --- a/crates/context-aware-config/tests/cac_tests.rs +++ b/crates/context-aware-config/tests/cac_tests.rs @@ -1,5 +1,6 @@ use context_aware_config::validation_functions::{compile_fn, execute_fn}; use serde_json::json; +use service_utils::result as superposition; // #[test] //todo : currently there is issue in running this test fn test_execute_fn() { @@ -24,17 +25,18 @@ fn test_execute_fn() { let err_execute = match execute_fn(&(execute_code_error.to_owned()), "test", json!(10)) { Ok(_) => false, - Err((e, stdout)) => e.contains("Bad schema"), + Err((e, _)) => e.contains("Bad schema"), }; let err_compile = match compile_fn(&(compile_code_error.to_owned())) { Ok(()) => false, - Err(e) => e.contains("Bad schema"), + Err(superposition::AppError::ValidationError(_)) => true, + _ => false, }; assert_eq!( execute_fn(&(code_ok.to_owned()), "test", json!(10)), Ok("true".to_string()) ); assert_eq!(err_execute, true); - assert_eq!(compile_fn(&(code_ok.to_owned())), Ok(())); + assert_eq!(compile_fn(&(code_ok.to_owned())).unwrap(), ()); assert_eq!(err_compile, true); } diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation-platform/Cargo.toml index b9e08f6a8..ffe615ced 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation-platform/Cargo.toml @@ -31,3 +31,4 @@ diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } service-utils = { path = "../service-utils" } reqwest = { workspace = true } dashboard-auth = { workspace = true } +anyhow = { workspace = true } \ No newline at end of file diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation-platform/src/api/experiments/handlers.rs index 81b9a9bb7..d2b29fbd9 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation-platform/src/api/experiments/handlers.rs @@ -12,16 +12,13 @@ use diesel::{ ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; -use service_utils::errors::types::{Error, ServerError}; - -use reqwest::{Response, StatusCode}; use service_utils::{ - errors::types::{Error as err, ErrorResponse}, - helpers::remove_error_abstraction, - service::types::{AppState, DbConnection, Tenant}, - types as app, + bad_argument, response_error, result as superposition, unexpected_error, }; +use reqwest::{Response, StatusCode}; +use service_utils::service::types::{AppState, DbConnection, Tenant}; + use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, @@ -30,9 +27,9 @@ use super::{ }, types::{ AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, - ContextMoveReq, ContextPutReq, ContextPutResp, ExperimentCreateRequest, - ExperimentCreateResponse, ExperimentResponse, ExperimentsResponse, ListFilters, - OverrideKeysUpdateRequest, RampRequest, Variant, + ContextMoveReq, ContextPutReq, ExperimentCreateRequest, ExperimentCreateResponse, + ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, + RampRequest, Variant, }, }; @@ -54,56 +51,46 @@ pub fn endpoints(scope: Scope) -> Scope { .service(update_overrides) } -async fn process_http_response( +async fn parse_error_response( + response: reqwest::Response, +) -> superposition::Result<(StatusCode, superposition::ErrorResponse)> { + let status_code = response.status(); + let error_response = response + .json::<superposition::ErrorResponse>() + .await + .map_err(|err: reqwest::Error| { + log::error!("failed to parse error response: {}", err); + unexpected_error!("Something went wrong") + })?; + log::error!("http call to CAC failed with err {:?}", error_response); + + Ok((status_code, error_response)) +} + +async fn process_cac_http_response( response: Result<Response, reqwest::Error>, -) -> Result<Vec<ContextPutResp>, Error> { +) -> superposition::Result<Vec<ContextBulkResponse>> { + let internal_server_error = unexpected_error!("Something went wrong."); match response { Ok(res) if res.status().is_success() => { - match res.json::<Vec<ContextBulkResponse>>().await { - Ok(bulk_responses) => { - let contexts = - bulk_responses - .into_iter() - .fold(Vec::new(), |mut acc, item| { - if let ContextBulkResponse::PUT(context) = item { - acc.push(context); - } else { - log::error!("Unexpected response item: {:?}", item); - } - acc - }); - Ok(contexts) - } - Err(e) => { - log::error!("Failed to parse JSON response: {}", e); - Err(Error::InternalServerErr(e.to_string())) - } - } + res.json::<Vec<ContextBulkResponse>>().await.map_err(|err| { + log::error!("failed to parse JSON response with error: {}", err); + internal_server_error + }) } Ok(res) => { - let error_response = ErrorResponse { - message: format!("HTTP error with status: {}", res.status().as_u16()), - possible_fix: String::from("Please check the request and try again."), - }; - match res.status() { - StatusCode::BAD_REQUEST => Err(Error::BadRequest(error_response)), - StatusCode::NOT_FOUND => Err(Error::NotFound(error_response)), - StatusCode::INTERNAL_SERVER_ERROR => Err(Error::InternalServerErr( - "Internal server error occurred".to_string(), - )), - _ => Err(Error::Generic(ServerError { - message: "Unexpected HTTP response".into(), - possible_fix: "Please contact support".into(), - status_code: res.status(), - })), + log::error!("http call to CAC failed with status_code {}", res.status()); + + if res.status().is_client_error() { + let (status_code, error_response) = parse_error_response(res).await?; + Err(response_error!(status_code, error_response.message)) + } else { + Err(internal_server_error) } } - Err(e) => { - log::error!("HTTP request failed: {}", e); - Err(Error::ConnectionFailed( - "External API".into(), - e.to_string(), - )) + Err(err) => { + log::error!("reqwest failed to send request to CAC with error: {}", err); + Err(internal_server_error) } } } @@ -115,7 +102,7 @@ async fn create( user: User, db_conn: DbConnection, tenant: Tenant, -) -> app::Result<Json<ExperimentCreateResponse>> { +) -> superposition::Result<Json<ExperimentCreateResponse>> { use crate::db::schema::experiments::dsl::experiments; let mut variants = req.variants.to_vec(); @@ -132,12 +119,10 @@ async fn create( HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); if unique_ids_of_variants_from_req.len() != variants.len() { - return Err(err::BadRequest(ErrorResponse { - message: "variant ids are expected to be unique".to_string(), - possible_fix: "provide unqiue variant IDs".to_string(), - })); + return Err(bad_argument!( + "Variant ids are expected to be unique. Provide unqiue variant IDs" + )); } - validate_override_keys(&unique_override_keys)?; // Checking if all the variants are overriding the mentioned keys @@ -148,19 +133,16 @@ async fn create( let are_valid_variants = check_variants_override_coverage(&variant_overrides, &unique_override_keys); if !are_valid_variants { - return Err(err::BadRequest(ErrorResponse { - message: "all variants should contain the keys mentioned in override_keys" - .to_string(), - possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", unique_override_keys.join(",")) - })); + return Err(bad_argument!( + "all variants should contain the keys mentioned in override_keys. Check if any of the following keys [{}] are missing from keys in your variants", + unique_override_keys.join(",") + ) + ); } // Checking if context is a key-value pair map if !req.context.is_object() { - return Err(err::BadRequest(ErrorResponse { - message: "context should be map of key value pairs".to_string(), - possible_fix: "Please refer documentation or contact an admin".to_string(), - })); + return Err(bad_argument!("Context should be map of key value pairs.")); } // validating experiment against other active experiments based on permission flags @@ -173,10 +155,7 @@ async fn create( &mut conn, )?; if !valid { - return Err(err::BadRequest(ErrorResponse { - message: reason, - possible_fix: "Please refer documentation or contact an admin".to_string(), - })); + return Err(bad_argument!(reason)); } // generating snowflake id for experiment @@ -197,9 +176,12 @@ async fn create( let payload = ContextPutReq { context: updated_cacccontext .as_object() - .ok_or(err::InternalServerErr( - "Could not convert updated CAC context to serde Object".to_string(), - ))? + .ok_or_else(|| { + log::error!("Could not convert updated CAC context to serde Object"); + unexpected_error!( + "Something went wrong, failed to create experiment contexts" + ) + })? .clone(), r#override: json!(variant.overrides), }; @@ -223,7 +205,17 @@ async fn create( .await; // directly return an error response if not a 200 response - let created_contexts = process_http_response(response).await?; + let created_contexts = process_cac_http_response(response).await?.into_iter().fold( + Vec::new(), + |mut acc, item| { + if let ContextBulkResponse::PUT(context) = item { + acc.push(context); + } else { + log::error!("Unexpected response item: {:?}", item); + } + acc + }, + ); for i in 0..created_contexts.len() { let created_context = &created_contexts[i]; variants[i].context_id = Some(created_context.context_id.clone()); @@ -264,7 +256,7 @@ async fn conclude_handler( db_conn: DbConnection, user: User, tenant: Tenant, -) -> app::Result<Json<ExperimentResponse>> { +) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(conn) = db_conn; let response = conclude( state, @@ -285,7 +277,7 @@ pub async fn conclude( mut conn: PooledConnection<ConnectionManager<PgConnection>>, user: User, tenant: Tenant, -) -> app::Result<Experiment> { +) -> superposition::Result<Experiment> { use crate::db::schema::experiments::dsl; let winner_variant_id: String = req.chosen_variant.to_owned(); @@ -295,30 +287,33 @@ pub async fn conclude( .get_result::<Experiment>(&mut conn)?; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { - return Err(err::BadRequest(ErrorResponse { - message: format!("experiment with id {} is already concluded", experiment_id), - possible_fix: "Try to conclude a different experiment".to_string(), - })); + return Err(bad_argument!( + "experiment with id {} is already concluded", + experiment_id + )); } - let experiment_context = - experiment - .context - .as_object() - .ok_or(err::InternalServerErr( - "Could not convert the context read from DB to JSON object".to_string(), - ))?; + let experiment_context = experiment.context.as_object().ok_or_else(|| { + log::error!("could not convert the context read from DB to JSON object"); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; let mut operations: Vec<ContextAction> = vec![]; - let experiment_variants: Vec<Variant> = - serde_json::from_value(experiment.variants) - .map_err(|e| err::InternalServerErr(e.to_string()))?; + let experiment_variants: Vec<Variant> = serde_json::from_value(experiment.variants) + .map_err(|err| { + log::error!( + "failed parse eixisting experiment variant while concluding with error: {}", + err + ); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; let mut is_valid_winner_variant = false; for variant in experiment_variants { - let context_id = variant.context_id.ok_or(err::InternalServerErr( - "Could not read context ID from experiment".to_string(), - ))?; + let context_id = variant.context_id.ok_or_else(|| { + log::error!("context id not available for variant {:?}", variant.id); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; if variant.id == winner_variant_id { let context_move_req = ContextMoveReq { @@ -335,12 +330,9 @@ pub async fn conclude( } if !is_valid_winner_variant { - return Err(err::NotFound(ErrorResponse { - message: "winner variant not found".to_string(), - possible_fix: - "A wrong variant ID may have been sent, please check and try again" - .to_string(), - })); + return Err(bad_argument!( + "winner variant not found. A wrong variant id may have been sent, check and try again" + )); } // calling CAC bulk api with operations as payload @@ -355,15 +347,9 @@ pub async fn conclude( .header("x-tenant", tenant.as_str()) .json(&operations) .send() - .await - .map_err(|e| remove_error_abstraction(e))?; + .await; - if response.status().is_server_error() { - return Err(err::InternalServerErr(format!( - "Request to {} failed with response: {:?}", - url, response - ))); - } + let _ = process_cac_http_response(response).await?; // updating experiment status in db let updated_experiment = diesel::update(dsl::experiments) @@ -384,7 +370,7 @@ async fn list_experiments( req: HttpRequest, filters: Query<ListFilters>, db_conn: DbConnection, -) -> app::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let max_event_timestamp: Option<NaiveDateTime> = event_log::event_log @@ -437,8 +423,8 @@ async fn list_experiments( let total_pages = (number_of_experiments as f64 / limit as f64).ceil() as i64; Ok(HttpResponse::Ok().json(ExperimentsResponse { + total_pages, total_items: number_of_experiments, - total_pages: total_pages, data: experiment_list .into_iter() .map(|entry| ExperimentResponse::from(entry)) @@ -450,7 +436,7 @@ async fn list_experiments( async fn get_experiment_handler( params: web::Path<i64>, db_conn: DbConnection, -) -> app::Result<Json<ExperimentResponse>> { +) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(mut conn) = db_conn; let response = get_experiment(params.into_inner(), &mut conn)?; return Ok(Json(ExperimentResponse::from(response))); @@ -459,7 +445,7 @@ async fn get_experiment_handler( pub fn get_experiment( experiment_id: i64, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, -) -> app::Result<Experiment> { +) -> superposition::Result<Experiment> { use crate::db::schema::experiments::dsl::*; let result: Experiment = experiments .find(experiment_id) @@ -474,7 +460,7 @@ async fn ramp( req: web::Json<RampRequest>, db_conn: DbConnection, user: User, -) -> app::Result<Json<ExperimentResponse>> { +) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); @@ -484,29 +470,29 @@ async fn ramp( let old_traffic_percentage = experiment.traffic_percentage as u8; let new_traffic_percentage = req.traffic_percentage as u8; - let experiment_variants: Vec<Variant> = - serde_json::from_value(experiment.variants) - .map_err(|e| err::InternalServerErr(e.to_string()))?; + let experiment_variants: Vec<Variant> = serde_json::from_value(experiment.variants) + .map_err(|e| { + log::error!( + "failed to parse existing experiment variants while ramping {}", + e + ); + unexpected_error!("Something went wrong, failed to ramp traffic percentage") + })?; let variants_count = experiment_variants.len() as u8; let max = 100 / variants_count; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { - return Err(err::BadRequest(ErrorResponse { - message: "Experiment is already concluded".to_string(), - possible_fix: "".to_string(), - })); + return Err(bad_argument!( + "experiment already concluded, cannot ramp a concluded experiment" + )); } else if new_traffic_percentage > max { - return Err(err::BadRequest(ErrorResponse { - message: format!("The traffic_percentage cannot exceed {}", max), - possible_fix: format!("Provide a traffic percentage less than {}", max), - })); + return Err(bad_argument!( + "The traffic_percentage cannot exceed {}. Provide a traffic percentage less than {}", max, max + ))?; } else if new_traffic_percentage != 0 && new_traffic_percentage == old_traffic_percentage { - return Err(err::BadRequest(ErrorResponse { - message: "The traffic_percentage is same as provided".to_string(), - possible_fix: "".to_string(), - })); + return Err(bad_argument!("The traffic_percentage is same as provided"))?; } let updated_experiment: Experiment = diesel::update(experiments::experiments) .filter(experiments::id.eq(exp_id)) @@ -529,17 +515,16 @@ async fn update_overrides( user: User, req: web::Json<OverrideKeysUpdateRequest>, tenant: Tenant, -) -> app::Result<Json<ExperimentResponse>> { +) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(mut conn) = db_conn; let experiment_id = params.into_inner(); let payload = req.into_inner(); let variants = payload.variants; - let first_variant = variants.get(0).ok_or(err::BadRequest(ErrorResponse { - message: "Variant not found in request".to_string(), - possible_fix: "Provide at least one entry in variant's list".to_string(), - }))?; + let first_variant = variants.get(0).ok_or(bad_argument!( + "Variant not found in request. Provide at least one entry in variant's list", + ))?; let override_keys = extract_override_keys(&first_variant.overrides) .into_iter() .collect(); @@ -550,18 +535,15 @@ async fn update_overrides( .first::<Experiment>(&mut conn)?; if experiment.status != ExperimentStatusType::CREATED { - return Err(err::BadRequest(ErrorResponse { - message: "Only experiments in CREATED state can be updated".to_string(), - possible_fix: "Please refer the documentation or contact an admin" - .to_string(), - })); + return Err(bad_argument!( + "Only experiments in CREATED state can be updated" + )); } let experiment_variants: Vec<Variant> = serde_json::from_value(experiment.variants) - .map_err(|e| { - err::InternalServerErr( - format!("Failed to parse exisitng variants: {e}").to_string(), - ) + .map_err(|err| { + log::error!("failed to parse exisiting variants with error {}", err); + unexpected_error!("Something went wrong, failed to update experiment") })?; let id_to_existing_variant: HashMap<String, &Variant> = HashMap::from_iter( @@ -584,16 +566,11 @@ async fn update_overrides( ); for existing_id in id_to_existing_variant.keys() { if !variant_ids.contains(existing_id) { - return Err(err::BadRequest(ErrorResponse { - message: - "some variant ids do not match with exisiting experiment variants" - .to_string(), - possible_fix: "provide all existing variants of the experiment" - .to_string(), - })); + Err(bad_argument!( + "Some variant ids do not match with exisiting experiment variants. Provide all existing variants of the experiment" + ))?; } } - // Checking if all the variants are overriding the mentioned keys let mut new_variants: Vec<Variant> = variants .into_iter() @@ -617,11 +594,12 @@ async fn update_overrides( let are_valid_variants = check_variants_override_coverage(&variant_overrides, &override_keys); if !are_valid_variants { - return Err(err::BadRequest(ErrorResponse { - message: "all variants should contain the keys mentioned in override_keys" - .to_string(), - possible_fix: format!("Check if any of the following keys [{}] are missing from keys in your variants", override_keys.join(",")) - })); + return Err( + bad_argument!( + "All variants should contain the keys mentioned in override_keys. Check if any of the following keys [{}] are missing from keys in your variants", + override_keys.join(",") + ) + )?; } // validating experiment against other active experiments based on permission flags @@ -634,10 +612,7 @@ async fn update_overrides( &mut conn, )?; if !valid { - return Err(err::BadRequest(ErrorResponse { - message: reason, - possible_fix: "Please refer documentation or contact an admin".to_string(), - })); + return Err(bad_argument!(reason)); } /******************************* Updating contexts ************************************/ @@ -645,13 +620,13 @@ async fn update_overrides( // adding operations to remove exisiting variant contexts for existing_variant in experiment_variants { - let context_id = existing_variant - .context_id - .ok_or(format!( - "Context Id not available for variant {:?}", + let context_id = existing_variant.context_id.ok_or_else(|| { + log::error!( + "context id not available for variant {:?}", existing_variant.id - )) - .map_err(|e| err::InternalServerErr(e))?; + ); + unexpected_error!("Something went wrong, failed to update experiment") + })?; cac_operations.push(ContextAction::DELETE(context_id.to_string())); } @@ -660,18 +635,17 @@ async fn update_overrides( let updated_cacccontext = add_variant_dimension_to_ctx(&experiment.context, variant.id.to_string()) .map_err(|e| { - err::InternalServerErr( - format!("failed to add variant dimension to context: {e}") - .to_string(), - ) + log::error!("failed to add `variantIds` dimension to context: {e}"); + unexpected_error!("Something went wrong, failed to update experiment") })?; let payload = ContextPutReq { context: updated_cacccontext .as_object() - .ok_or(err::InternalServerErr( - "failed to parse updated context with variant dimension".to_string(), - ))? + .ok_or_else(|| { + log::error!("failed to parse updated context with variant dimension"); + unexpected_error!("Something went wrong, failed to update experiment") + })? .clone(), r#override: json!(variant.overrides), }; @@ -693,7 +667,17 @@ async fn update_overrides( .await; // directly return an error response if not a 200 response - let created_contexts = process_http_response(response).await?; + let created_contexts = process_cac_http_response(response).await?.into_iter().fold( + Vec::new(), + |mut acc, item| { + if let ContextBulkResponse::PUT(context) = item { + acc.push(context); + } else { + log::error!("Unexpected response item: {:?}", item); + } + acc + }, + ); for i in 0..created_contexts.len() { let created_context = &created_contexts[i]; @@ -702,9 +686,9 @@ async fn update_overrides( } /*************************** Updating experiment in DB **************************/ - let new_variants_json = serde_json::to_value(new_variants).map_err(|e| { - err::InternalServerErr(format!("failed to convert new variants to json: {e}")) + log::error!("failed to serialize new variants to json with error: {e}"); + bad_argument!("failed to update experiment, bad variant data") })?; let updated_experiment = diesel::update(experiments::experiments.find(experiment_id)) .set(( @@ -722,7 +706,7 @@ async fn update_overrides( async fn get_audit_logs( filters: Query<AuditQueryFilters>, db_conn: DbConnection, -) -> app::Result<HttpResponse> { +) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let query_builder = |filters: &AuditQueryFilters| { diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation-platform/src/api/experiments/helpers.rs index b918c2e0e..64a6cbe0d 100644 --- a/crates/experimentation-platform/src/api/experiments/helpers.rs +++ b/crates/experimentation-platform/src/api/experiments/helpers.rs @@ -3,13 +3,13 @@ use crate::db::models::{Experiment, ExperimentStatusType}; use diesel::pg::PgConnection; use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::{Map, Value}; -use service_utils::errors::types::ErrorResponse; use service_utils::helpers::extract_dimensions; use service_utils::service::types::ExperimentationFlags; -use service_utils::{errors::types::Error as err, types as app}; use std::collections::HashSet; -pub fn check_variant_types(variants: &Vec<Variant>) -> app::Result<()> { +use service_utils::{bad_argument, result as superposition}; + +pub fn check_variant_types(variants: &Vec<Variant>) -> superposition::Result<()> { let mut experimental_variant_cnt = 0; let mut control_variant_cnt = 0; @@ -25,28 +25,25 @@ pub fn check_variant_types(variants: &Vec<Variant>) -> app::Result<()> { } if control_variant_cnt > 1 || control_variant_cnt == 0 { - return Err(err::BadArgument(ErrorResponse { - message: "experiment should have exactly 1 control variant".to_string(), - possible_fix: "ensure only one control variant is present".to_string(), - })); + return Err(bad_argument!( + "Experiment should have exactly 1 control variant. Ensure only one control variant is present" + )); } else if experimental_variant_cnt < 1 { - return Err(err::BadArgument(ErrorResponse { - message: "experiment should have at least 1 experimental variant".to_string(), - possible_fix: "ensure only one control variant is present".to_string(), - })); + return Err(bad_argument!( + "Experiment should have at least 1 experimental variant. Ensure only one control variant is present" + )); } Ok(()) } -pub fn validate_override_keys(override_keys: &Vec<String>) -> app::Result<()> { +pub fn validate_override_keys(override_keys: &Vec<String>) -> superposition::Result<()> { let mut key_set: HashSet<&str> = HashSet::new(); for key in override_keys { if !key_set.insert(key) { - return Err(err::BadArgument(ErrorResponse { - message: "override_keys are not unique".to_string(), - possible_fix: "remove duplicate entries in override_keys".to_string(), - })); + return Err(bad_argument!( + "override_keys are not unique. Remove duplicate entries in override_keys" + )); } } @@ -56,7 +53,7 @@ pub fn validate_override_keys(override_keys: &Vec<String>) -> app::Result<()> { pub fn are_overlapping_contexts( context_a: &Value, context_b: &Value, -) -> app::Result<bool> { +) -> superposition::Result<bool> { let dimensions_a = extract_dimensions(context_a)?; let dimensions_b = extract_dimensions(context_b)?; @@ -117,7 +114,7 @@ pub fn is_valid_experiment( override_keys: &Vec<String>, flags: &ExperimentationFlags, active_experiments: &Vec<Experiment>, -) -> app::Result<(bool, String)> { +) -> superposition::Result<(bool, String)> { let mut valid_experiment = true; let mut invalid_reason = String::new(); if !flags.allow_same_keys_overlapping_ctx @@ -129,11 +126,10 @@ pub fn is_valid_experiment( let are_overlapping = are_overlapping_contexts(context, &active_experiment.context) .map_err(|e| { - log::info!("validate_experiment: {e}"); - err::BadArgument(ErrorResponse { - message: "Failed to validate for overlapping context. One of the current running experiments already has this context or overlaps with it".into(), - possible_fix: "Overlapping contexts are not allowed currently as per your configuration of CAC".into(), - }) + log::info!("experiment validation failed with error: {e}"); + bad_argument!( + "Context overlap validation failed, given context overlaps with a running experiment's context. Overlapping contexts are not allowed currently as per your configuration" + ) })?; let have_intersecting_key_set = active_experiment @@ -175,7 +171,7 @@ pub fn validate_experiment( experiment_id: Option<i64>, flags: &ExperimentationFlags, conn: &mut PgConnection, -) -> app::Result<(bool, String)> { +) -> superposition::Result<(bool, String)> { use crate::db::schema::experiments::dsl as experiments_dsl; let active_experiments: Vec<Experiment> = experiments_dsl::experiments @@ -195,23 +191,17 @@ pub fn validate_experiment( pub fn add_variant_dimension_to_ctx( context_json: &Value, variant: String, -) -> app::Result<Value> { - let context = context_json - .as_object() - .ok_or(err::BadArgument(ErrorResponse { - message: "context not an object".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; +) -> superposition::Result<Value> { + let context = context_json.as_object().ok_or(bad_argument!( + "Context not an object. Ensure the context provided obeys the rules of JSON logic" + ))?; let mut conditions = match context.get("and") { Some(conditions_json) => conditions_json .as_array() - .ok_or(err::BadArgument(ErrorResponse { - message: " failed parsing conditions as an array".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))? + .ok_or(bad_argument!( + "Failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic" + ))? .clone(), None => vec![context_json.clone()], }; @@ -229,10 +219,9 @@ pub fn add_variant_dimension_to_ctx( match serde_json::to_value(updated_ctx) { Ok(value) => Ok(value), - Err(_) => Err(err::BadArgument(ErrorResponse { - message: "Failed to convert context to a valid JSON object".to_string(), - possible_fix: "Check the request sent for correctness".to_string(), - })), + Err(_) => Err(bad_argument!( + "Failed to convert context to a valid JSON object. Check the request sent for correctness" + )), } } diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation-platform/tests/experimentation_tests.rs index a93c72d94..af7f8d1ed 100644 --- a/crates/experimentation-platform/tests/experimentation_tests.rs +++ b/crates/experimentation-platform/tests/experimentation_tests.rs @@ -2,8 +2,8 @@ use chrono::Utc; use experimentation_platform::api::experiments::helpers; use experimentation_platform::db::models::{Experiment, ExperimentStatusType}; use serde_json::{json, Map, Value}; -use service_utils::errors::types::Error as AppError; use service_utils::helpers::extract_dimensions; +use service_utils::result::AppError; use service_utils::service::types::ExperimentationFlags; enum Dimensions { diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index f3dc37bad..f568abdfe 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -23,3 +23,4 @@ service-utils = { path = "../service-utils" } reqwest = { workspace = true } experimentation-platform = { path = "../experimentation-platform"} dashboard-auth = { workspace = true } +anyhow = { workspace = true } \ No newline at end of file diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index d89f1f259..e19180af8 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -19,8 +19,8 @@ use experimentation_platform::{ use serde_json::Value; use service_utils::{ helpers::extract_dimensions, + result, service::types::{AppState, DbConnection, Tenant}, - types as app, }; pub fn endpoints(scope: Scope) -> Scope { @@ -37,7 +37,7 @@ async fn stabilize( db_conn: DbConnection, user: User, tenant: Tenant, -) -> app::Result<Json<ExperimentResponse>> { +) -> result::Result<Json<ExperimentResponse>> { let response = conclude_experiment( params.into_inner(), state, @@ -57,7 +57,7 @@ async fn revert( db_conn: DbConnection, user: User, tenant: Tenant, -) -> app::Result<Json<ExperimentResponse>> { +) -> result::Result<Json<ExperimentResponse>> { let response = conclude_experiment( params.into_inner(), state, @@ -77,7 +77,7 @@ pub async fn conclude_experiment( user: User, tenant: Tenant, variant: VariantType, -) -> app::Result<Experiment> { +) -> result::Result<Experiment> { let DbConnection(mut conn) = db_conn; let experiment = get_experiment(exp_id, &mut conn)?; @@ -94,7 +94,7 @@ pub async fn diff_handler( params: web::Path<i64>, state: Data<AppState>, db_conn: DbConnection, -) -> app::Result<Json<DiffResponse>> { +) -> result::Result<Json<DiffResponse>> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); let experiment = get_experiment(exp_id, &mut conn)?; diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs index fe8c47f51..7d19c57d9 100644 --- a/crates/external/src/api/external_api/helpers.rs +++ b/crates/external/src/api/external_api/helpers.rs @@ -5,19 +5,17 @@ use experimentation_platform::{ }; use serde_json::{Map, Value}; -use service_utils::{ - errors::types::Error as err, service::types::AppState, types as app, -}; +use service_utils::{result, service::types::AppState, unexpected_error}; pub fn fetch_variant_id( experiment: &Experiment, variant: VariantType, -) -> app::Result<String> { +) -> result::Result<String> { let variants = &experiment.variants; let experiment_variants: Vec<Variant> = serde_json::from_value(variants.clone()) .map_err(|e| { log::error!("parsing to variant type failed with err: {e}"); - err::InternalServerErr("".to_string()) + unexpected_error!("Something went wrong.") })?; for ele in experiment_variants { @@ -30,13 +28,13 @@ pub fn fetch_variant_id( variant, experiment.id ); - return Err(err::InternalServerErr("".to_string())); + return Err(unexpected_error!("Something went wrong.")); } pub async fn get_resolved_config( state: &Data<AppState>, dimension_ctx: &Map<String, Value>, -) -> app::Result<Value> { +) -> result::Result<Value> { let http_client = reqwest::Client::new(); let url = format!("{}/config/resolve", state.cac_host); let resp = http_client @@ -46,9 +44,9 @@ pub async fn get_resolved_config( .query(dimension_ctx) .send() .await - .map_err(|e| err::InternalServerErr(e.to_string()))? + .map_err(|e| unexpected_error!(e))? .json() .await - .map_err(|e| err::InternalServerErr(e.to_string()))?; + .map_err(|e| unexpected_error!(e))?; Ok(resp) } diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs index dab217785..12d067e31 100644 --- a/crates/frontend/src/components/experiment/experiment.rs +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use leptos::*; -use crate::components::condition_pills::utils::{extract_and_format, parse_conditions}; +use crate::components::condition_pills::utils::extract_and_format; use crate::components::table::table::Table; use super::utils::gen_variant_table; diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index ede8d4d93..2316a0b2f 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -7,8 +7,6 @@ pub mod types; mod utils; use cfg_if::cfg_if; -use utils::use_env; - cfg_if! { if #[cfg(feature = "hydrate")] { use wasm_bindgen::prelude::wasm_bindgen; @@ -20,7 +18,7 @@ cfg_if! { console_error_panic_hook::set_once(); - let envs = use_env(); + let envs = utils::use_env(); leptos::mount_to_body(move || { view! { <App app_envs={envs.clone()} /> } diff --git a/crates/service-utils/src/errors/mod.rs b/crates/service-utils/src/errors/mod.rs deleted file mode 100644 index cd408564e..000000000 --- a/crates/service-utils/src/errors/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod types; diff --git a/crates/service-utils/src/errors/types.rs b/crates/service-utils/src/errors/types.rs deleted file mode 100644 index c513d16fe..000000000 --- a/crates/service-utils/src/errors/types.rs +++ /dev/null @@ -1,151 +0,0 @@ -use actix_web::{ - error, - http::{header::ContentType, StatusCode}, - web::Json, - HttpResponse, -}; -use derive_more::{Display, Error}; -use serde::{Deserialize, Serialize}; -use std::error as err; - -#[derive(Debug, Error, Display, Clone)] -#[display( - fmt = "server returned an error: {} with status code {}", - message, - status_code -)] -pub struct ServerError { - pub message: String, - pub possible_fix: String, - pub status_code: StatusCode, -} - -impl error::ResponseError for ServerError { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code) - .insert_header(ContentType::json()) - .json(Json(ErrorResponse { - message: self.message.to_string(), - possible_fix: self.possible_fix.to_string(), - })) - } -} - -#[derive(Deserialize, Serialize, Debug, Display, Error, Clone)] -#[display( - fmt = "error sent to client {} that can be fixed by {}", - message, - possible_fix -)] -pub struct ErrorResponse { - pub message: String, - pub possible_fix: String, -} - -#[derive(Debug, Display, Clone)] -pub enum Error { - #[display(fmt = "Connection failed to {}, reason: {}", "_0", "_1")] - ConnectionFailed(String, String), - #[display(fmt = "Error occured with postgres: {}", "_0")] - DB(String), - #[display(fmt = "The resource(s) requested were not found: {}", "_0")] - NotFound(ErrorResponse), - #[display(fmt = "Bad Request: {}", "_0")] - BadRequest(ErrorResponse), - #[display(fmt = "Bad Arugment: {}", "_0")] - BadArgument(ErrorResponse), - #[display(fmt = "Something went wrong, reason: {}", "_0")] - InternalServerErr(String), - #[display(fmt = "{}", "_0")] - Generic(ServerError), -} - -impl err::Error for Error {} - -impl From<diesel::result::Error> for Error { - fn from(value: diesel::result::Error) -> Self { - log::error!("{}", value); - match value { - diesel::result::Error::InvalidCString(e) => { - Self::ConnectionFailed("DATABASE".into(), e.to_string()) - } - diesel::result::Error::DatabaseError(kind, error) => Self::DB(format!( - "Database error of kind {:?} occurred due to {}", - kind, - error.message() - )), - diesel::result::Error::NotFound => Self::NotFound(ErrorResponse { - message: "No records found".into(), - possible_fix: "Please refine or correct your search parameters".into(), - }), - diesel::result::Error::DeserializationError(_) => { - Self::BadRequest(ErrorResponse { - message: "The server could not understand your request".into(), - possible_fix: "Please check your request for issues".into(), - }) - } - diesel::result::Error::SerializationError(_) => { - Self::BadRequest(ErrorResponse { - message: "The server could not understand your request".into(), - possible_fix: "Please check your request for issues".into(), - }) - } - e => Self::InternalServerErr(format!("What went wrong: {:?}", e)), - } - } -} - -impl Error { - fn generate_err_response( - code: StatusCode, - message: String, - possible_fix: String, - ) -> HttpResponse { - HttpResponse::build(code) - .insert_header(ContentType::json()) - .json(Json(ErrorResponse { - message, - possible_fix, - })) - } -} - -impl error::ResponseError for Error { - fn error_response(&self) -> HttpResponse { - let please_try_again: String = String::from("Please try again later"); - log::error!("{}", self); - match self { - Error::ConnectionFailed(_, _) => Self::generate_err_response( - StatusCode::SERVICE_UNAVAILABLE, - "Something went wrong".into(), - please_try_again, - ), - Error::DB(_) => Self::generate_err_response( - StatusCode::INTERNAL_SERVER_ERROR, - "Something went wrong".into(), - please_try_again, - ), - Error::NotFound(reason) => Self::generate_err_response( - StatusCode::NOT_FOUND, - reason.message.clone(), - reason.possible_fix.clone(), - ), - Error::BadRequest(cause) => Self::generate_err_response( - StatusCode::BAD_REQUEST, - cause.message.clone(), - cause.possible_fix.clone(), - ), - Error::BadArgument(cause) => Self::generate_err_response( - StatusCode::BAD_REQUEST, - cause.message.clone(), - cause.possible_fix.clone(), - ), - Error::InternalServerErr(_) => Self::generate_err_response( - StatusCode::INTERNAL_SERVER_ERROR, - "Something went wrong".into(), - please_try_again, - ), - Error::Generic(server_error) => server_error.error_response(), - } - } -} diff --git a/crates/service-utils/src/helpers.rs b/crates/service-utils/src/helpers.rs index 8d2080305..70e40d8c9 100644 --- a/crates/service-utils/src/helpers.rs +++ b/crates/service-utils/src/helpers.rs @@ -1,4 +1,4 @@ -use actix_web::{error::ErrorInternalServerError, http::StatusCode, Error}; +use actix_web::{error::ErrorInternalServerError, Error}; use log::info; use serde::de::{self, IntoDeserializer}; use std::{ @@ -7,8 +7,7 @@ use std::{ str::FromStr, }; -use super::errors::types::{Error as err, ErrorResponse}; -use crate::types as app; +use super::result; use serde_json::{Map, Value}; //WARN Do NOT use this fxn inside api requests, instead add the required @@ -124,28 +123,18 @@ pub fn get_pod_info() -> (String, String) { (pod_id, deployment_id) } -pub fn remove_error_abstraction(e: reqwest::Error) -> err { - match e.status() { - Some(StatusCode::BAD_REQUEST) => err::BadRequest(ErrorResponse { - message: e.to_string(), - possible_fix: - "Please try again with correct value. Schema type / Validation is failing" - .to_string(), - }), - _ => err::InternalServerErr(e.to_string()), - } -} - -pub fn extract_dimensions(context_json: &Value) -> app::Result<Map<String, Value>> { +pub fn extract_dimensions(context_json: &Value) -> result::Result<Map<String, Value>> { // Assuming max 2-level nesting in context json logic let context = context_json .as_object() - .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: context not a valid JSON object".to_string(), possible_fix: "send a valid JSON context".to_string() }))?; + .ok_or( + result::AppError::BadArgument("Error extracting dimensions, contect not a valid JSON object. Provide a valid JSON context".into()) + )?; let conditions = match context.get("and") { Some(conditions_json) => conditions_json .as_array() - .ok_or(err::BadArgument(ErrorResponse { message: "An error occurred while extracting dimensions: failed parsing conditions as an array".to_string(), possible_fix: "ensure the context provided obeys the rules of JSON logic".to_string() }))? + .ok_or(result::AppError::BadArgument("Error extracting dimensions, failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic".into()))? .clone(), None => vec![context_json.clone()], }; @@ -155,22 +144,15 @@ pub fn extract_dimensions(context_json: &Value) -> app::Result<Map<String, Value let condition_obj = condition .as_object() - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to parse condition as an object".to_string(), - possible_fix: - "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; + .ok_or(result::AppError::BadArgument( + "Failed to parse condition as an object. Ensure the context provided obeys the rules of JSON logic".to_string() + ))?; let operators = condition_obj.keys(); for operator in operators { - let operands = condition_obj[operator].as_array().ok_or(err::BadArgument( - ErrorResponse { - message: " failed to parse operands as an arrays".to_string(), - possible_fix: - "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }, + let operands = condition_obj[operator].as_array().ok_or(result::AppError::BadArgument( + "Failed to parse operands as an arrays. Ensure the context provided obeys the rules of JSON logic" + .into() ))?; let (variable_name, variable_value) = get_variable_name_and_value(operands)?; @@ -182,38 +164,37 @@ pub fn extract_dimensions(context_json: &Value) -> app::Result<Map<String, Value Ok(Map::from_iter(dimension_tuples)) } -pub fn get_variable_name_and_value(operands: &Vec<Value>) -> app::Result<(&str, &Value)> { +pub fn get_variable_name_and_value( + operands: &Vec<Value>, +) -> result::Result<(&str, &Value)> { let (obj_pos, variable_obj) = operands .iter() .enumerate() .find(|(_, operand)| { operand.is_object() && operand.as_object().unwrap().get("var").is_some() }) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable name from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; + .ok_or(result::AppError::BadArgument( + "Failed to get variable name from operands list. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; let variable_name = variable_obj .as_object() .map_or(None, |obj| obj.get("var")) .map_or(None, |value| value.as_str()) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable name from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; + .ok_or(result::AppError::BadArgument( + "Failed to get variable name as string. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; let value_pos = (obj_pos + 1) % 2; let variable_value = operands .get(value_pos) - .ok_or(err::BadArgument(ErrorResponse { - message: " failed to get variable value from operands list".to_string(), - possible_fix: "ensure the context provided obeys the rules of JSON logic" - .to_string(), - }))?; + .ok_or(result::AppError::BadArgument( + "Failed to get variable value from operands list. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; Ok((variable_name, variable_value)) } diff --git a/crates/service-utils/src/lib.rs b/crates/service-utils/src/lib.rs index 96e392401..266b1f465 100644 --- a/crates/service-utils/src/lib.rs +++ b/crates/service-utils/src/lib.rs @@ -1,9 +1,7 @@ pub mod aws; pub mod db; -pub mod errors; pub mod helpers; +pub mod macros; pub mod middlewares; -pub mod service; -pub mod types; pub mod result; -pub mod macros; \ No newline at end of file +pub mod service; diff --git a/crates/service-utils/src/macros.rs b/crates/service-utils/src/macros.rs index a125c6c33..1c4f96290 100644 --- a/crates/service-utils/src/macros.rs +++ b/crates/service-utils/src/macros.rs @@ -1,8 +1,5 @@ #[macro_export] macro_rules! bad_argument { - ($msg: literal) => { - service_utils::result::AppError::BadArgument($msg.into()) - }; ($msg: literal, $($args: tt)*) => { service_utils::result::AppError::BadArgument(format!($msg, $($args)*)) }; @@ -13,9 +10,6 @@ macro_rules! bad_argument { #[macro_export] macro_rules! validation_error { - ($msg: literal) => { - service_utils::result::AppError::ValidationError($msg.into()) - }; ($msg: literal, $($args: tt)*) => { service_utils::result::AppError::ValidationError(format!($msg, $($args)*)) }; @@ -26,9 +20,6 @@ macro_rules! validation_error { #[macro_export] macro_rules! unexpected_error { - ($msg: literal) => { - service_utils::result::AppError::UnexpectedError(anyhow::anyhow!($msg)) - }; ($msg: literal, $($args: tt)*) => { service_utils::result::AppError::UnexpectedError(anyhow::anyhow!(format!($msg, $($args)*))) }; @@ -37,6 +28,16 @@ macro_rules! unexpected_error { }; } +#[macro_export] +macro_rules! not_found { + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::NotFound(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::NotFound($err.to_string()) + }; +} + #[macro_export] macro_rules! db_error { ($error: expr) => { @@ -45,11 +46,13 @@ macro_rules! db_error { } #[macro_export] -macro_rules! server_error { +macro_rules! response_error { ($status: expr, $msg: expr) => { - service_utils::result::AppError::ServerError(service_utils::result::ServerError { - status_code: $status, - message: $msg, - }) + service_utils::result::AppError::ResponseError( + service_utils::result::ResponseError { + status_code: $status, + message: $msg.to_string(), + }, + ) }; -} \ No newline at end of file +} diff --git a/crates/service-utils/src/result.rs b/crates/service-utils/src/result.rs index a3ca0a542..90901c54f 100644 --- a/crates/service-utils/src/result.rs +++ b/crates/service-utils/src/result.rs @@ -4,7 +4,7 @@ use actix_web::{ HttpResponse, }; use derive_more::Display; -use serde_json::json; +use serde::{Deserialize, Serialize}; use thiserror::Error as this_error; #[derive(this_error)] @@ -13,10 +13,12 @@ pub enum AppError { ValidationError(String), #[error("bad arguments ( `{0}` )")] BadArgument(String), + #[error("not found ( `{0}` )")] + NotFound(String), #[error(transparent)] DbError(#[from] diesel::result::Error), #[error(transparent)] - ServerError(#[from] ServerError), + ResponseError(#[from] ResponseError), #[error(transparent)] UnexpectedError(anyhow::Error), } @@ -27,16 +29,23 @@ pub enum AppError { message, status_code )] -pub struct ServerError { +pub struct ResponseError { pub message: String, pub status_code: StatusCode, } +#[derive(Debug, Clone, Serialize, Deserialize, Display)] +pub struct ErrorResponse { + pub message: String, +} + pub type Result<T> = core::result::Result<T, AppError>; impl AppError { fn generate_err_response(code: StatusCode, msg: &str) -> HttpResponse { - let response = json!({ "message" : msg }); + let response = ErrorResponse { + message: msg.into(), + }; HttpResponse::build(code) .insert_header(ContentType::json()) .json(response) @@ -50,16 +59,19 @@ impl error::ResponseError for AppError { log::error!("{}", self); match self { - AppError::ValidationError(msg) => { + AppError::ValidationError(msg) | AppError::BadArgument(msg) => { Self::generate_err_response(StatusCode::BAD_REQUEST, msg) } - AppError::BadArgument(msg) => { - Self::generate_err_response(StatusCode::BAD_REQUEST, msg) + AppError::NotFound(msg) => { + Self::generate_err_response(StatusCode::NOT_FOUND, msg) } AppError::UnexpectedError(_) => Self::generate_err_response( StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong", ), + AppError::ResponseError(error) => { + Self::generate_err_response(error.status_code, &error.message) + } AppError::DbError(diesel_error::InvalidCString(_)) => { Self::generate_err_response( @@ -85,10 +97,6 @@ impl error::ResponseError for AppError { StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong", ), - - AppError::ServerError(error) => { - Self::generate_err_response(error.status_code, &error.message) - } } } } @@ -110,4 +118,4 @@ impl std::fmt::Debug for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { error_chain_fmt(self, f) } -} \ No newline at end of file +} diff --git a/crates/service-utils/src/types.rs b/crates/service-utils/src/types.rs deleted file mode 100644 index 448819675..000000000 --- a/crates/service-utils/src/types.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Result<T> = core::result::Result<T, crate::errors::types::Error>; From 4f9f9a6e9e3cd3c5e182ccc31b350a4f3d4109d8 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 10 Apr 2024 09:16:15 +0000 Subject: [PATCH 337/352] chore(version): v0.37.0 [skip ci] --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- crates/service-utils/CHANGELOG.md | 8 ++++++++ crates/service-utils/Cargo.toml | 4 ++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d23e3d01f..de7e01c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.37.0 - 2024-04-10 +### Package updates +- service-utils bumped to service-utils-v0.13.0 +### Global changes +#### Features +- [PICAF-25423] added new result, error type and error macros - (e673fb1) - Shubhranshu Sanjeev +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + ## v0.36.1 - 2024-04-08 ### Package updates - context-aware-config bumped to context-aware-config-v0.25.1 diff --git a/Cargo.lock b/Cargo.lock index 687382bc2..609726321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3535,7 +3535,7 @@ dependencies = [ [[package]] name = "service-utils" -version = "0.12.0" +version = "0.13.0" dependencies = [ "actix", "actix-web", diff --git a/crates/service-utils/CHANGELOG.md b/crates/service-utils/CHANGELOG.md index 8e960efe0..e09f4a1ac 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service-utils/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## service-utils-v0.13.0 - 2024-04-10 +#### Features +- [PICAF-25423] added new result, error type and error macros - (e673fb1) - Shubhranshu Sanjeev +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + ## service-utils-v0.12.0 - 2024-03-08 #### Features - PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato diff --git a/crates/service-utils/Cargo.toml b/crates/service-utils/Cargo.toml index 71928d155..93e4d4320 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service-utils" -version = "0.12.0" +version = "0.13.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,4 +33,4 @@ serde_json = { workspace = true } derive_more = { workspace = true } dashboard-auth = { workspace = true } reqwest = { workspace = true } -thiserror = { workspace = true } \ No newline at end of file +thiserror = { workspace = true } From 59ecc4ce453dfbd2e28f15eb6d0eab5213358250 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Fri, 5 Apr 2024 18:53:39 +0530 Subject: [PATCH 338/352] feat: Add function support in dimension and default config form --- .../default_config_form.rs | 126 +++++++++-- .../components/default_config_form/types.rs | 1 + .../dimension_form/dimension_form.rs | 101 ++++++++- .../src/components/dimension_form/types.rs | 1 + .../frontend/src/components/drawer/drawer.rs | 16 +- .../src/components/dropdown/dropdown.rs | 21 +- .../src/pages/DefaultConfig/DefaultConfig.rs | 103 +++++---- .../src/pages/Dimensions/Dimensions.rs | 198 ++++++++---------- .../pages/experiment_list/experiment_list.rs | 4 +- crates/frontend/src/types.rs | 12 ++ 10 files changed, 377 insertions(+), 206 deletions(-) diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 027ed69db..4f94b4bc7 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -3,7 +3,15 @@ use serde_json::{json, Number, Value}; use std::str::FromStr; use web_sys::MouseEvent; -use crate::{components::button::button::Button, utils::parse_string_to_json_value_vec}; +use crate::{ + api::fetch_functions, + components::{ + button::button::Button, + dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + }, + types::FunctionsName, + utils::parse_string_to_json_value_vec, +}; use super::{types::DefaultConfigCreateReq, utils::create_default_config}; @@ -14,6 +22,7 @@ pub fn default_config_form<NF>( #[prop(default = String::new())] config_type: String, #[prop(default = String::new())] config_pattern: String, #[prop(default = String::new())] config_value: String, + #[prop(default = None)] function_name: Option<Value>, handle_submit: NF, ) -> impl IntoView where @@ -25,6 +34,30 @@ where let (config_type, set_config_type) = create_signal(config_type); let (config_pattern, set_config_pattern) = create_signal(config_pattern); let (config_value, set_config_value) = create_signal(config_value); + let (function_name, set_function_name) = create_signal(function_name); + + let functions_resource: Resource<String, Vec<crate::types::FunctionResponse>> = + create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_functions(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let handle_select_dropdown_option = move |selected_function: FunctionsName| { + set_function_name.update(|value| { + let function_name = selected_function.clone(); + leptos::logging::log!("function selected: {:?}", function_name); + let fun_name = match function_name.as_str() { + "None" => None, + _ => Some(json!(function_name)), + }; + *value = fun_name; + }); + }; let (show_labels, set_show_labels) = create_signal(edit); @@ -36,6 +69,7 @@ where let f_type = config_type.get(); let f_pattern = config_pattern.get(); let f_value = config_value.get(); + let fun_name = function_name.get(); let f_value = match f_type.as_str() { "number" => Value::Number(f_value.parse::<i64>().unwrap().into()), @@ -52,6 +86,7 @@ where Ok(boolean) => Value::Bool(boolean), _ => Value::String("Invalid Boolean".to_string()), }, + "pattern" | "enum" => Value::String(f_value), _ => Value::from_str(&f_value).expect("Error parsing JSON"), }; @@ -90,6 +125,7 @@ where let payload = DefaultConfigCreateReq { schema: f_schema, value: f_value, + function_name: fun_name, }; let handle_submit_clone = handle_submit.clone(); @@ -119,14 +155,14 @@ where view! { <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Key</span> + <label class="label"> + <span class="label-text">Key Name</span> </label> <input disabled=edit type="text" placeholder="Key" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md" value=config_key.get() on:change=move |ev| { let value = event_target_value(&ev); @@ -136,6 +172,12 @@ where </div> + <div class="divider"></div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Set Schema</span> + </label> <select name="schemaType[]" on:change=move |ev| { @@ -165,10 +207,10 @@ where }; } - class="select select-bordered" + class="select select-bordered w-full max-w-md" > <option disabled selected> - Set Schema + Choose Schema Type </option> <option @@ -202,6 +244,9 @@ where "Other" </option> </select> + </div> + + <div class="divider"></div> {move || { view! { @@ -209,13 +254,13 @@ where (config_type.get() == "number") || (config_type.get() == "decimal") }> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> + <label class="label"> + <span class="label-text">Value</span> </label> <input type="number" placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md" value=config_value.get() on:change=move |ev| { logging::log!("{:?}", event_target_value(& ev)); @@ -224,20 +269,20 @@ where /> </div> + <div class="divider"></div> </Show> - <Show when=move || { - show_labels.get() && (config_type.get() != "number") + <Show when=move || { show_labels.get() && (config_type.get() != "number") && (config_type.get() != "decimal") }> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Value</span> + <label class="label"> + <span class="label-text ">Value</span> </label> <input type="text" placeholder="Value" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md" value=config_value.get() on:change=move |ev| { logging::log!("{:?}", event_target_value(& ev)); @@ -246,16 +291,19 @@ where /> </div> + + <div class="divider"></div> + <Show when=move || (config_type.get() != "boolean")> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono"> + <label class="label"> + <span class="label-text"> {config_type.get()} </span> </label> <textarea type="text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md pt-[10px]" on:change=move |ev| { let value = event_target_value(&ev); logging::log!("{:?}", value); @@ -265,15 +313,53 @@ where {config_pattern.get()} </textarea> - </div> + <div class="divider"></div> </Show> + </Show> } }} - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click=on_submit/> + <Suspense> + {move || { + let functions = functions_resource.get().unwrap_or(vec![]); + let mut function_names: Vec<FunctionsName> = vec![]; + functions.into_iter().for_each(|ele| { + function_names.push(ele.function_name); + }); + function_names.sort(); + function_names.insert(0, "None".to_string()); + view! { + <div class="form-control"> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> + <span class="label-text">Function Name</span> + <span class="label-text text-slate-400">Assign Function validation to your key</span> + </label> + </div> + + <div class="mt-2"> + <Dropdown + dropdown_width="w-100" + dropdown_icon="".to_string() + dropdown_text={function_name.get().and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }).map_or("Add Function".to_string(), |v| v.to_string())} + dropdown_direction=DropdownDirection::Down + dropdown_btn_type=DropdownBtnType::Select + dropdown_options=function_names + on_select=Box::new(handle_select_dropdown_option) + /> + </ div> + </ div> + } + }} + </ Suspense> + + <div class="form-control grid w-full justify-end"> + <Button class="pl-[70px] pr-[70px]".to_string() text="Submit".to_string() on_click=on_submit/> </div> { diff --git a/crates/frontend/src/components/default_config_form/types.rs b/crates/frontend/src/components/default_config_form/types.rs index b95655b4f..3a86a7bcf 100644 --- a/crates/frontend/src/components/default_config_form/types.rs +++ b/crates/frontend/src/components/default_config_form/types.rs @@ -5,4 +5,5 @@ use serde_json::Value; pub struct DefaultConfigCreateReq { pub schema: Value, pub value: Value, + pub function_name: Option<Value>, } diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index f0eee2b08..cd9114184 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -1,7 +1,11 @@ use super::types::DimensionCreateReq; use super::utils::create_dimension; -use crate::components::button::button::Button; +use crate::components::dropdown::dropdown::{ + Dropdown, DropdownBtnType, DropdownDirection, +}; +use crate::types::FunctionsName; use crate::utils::parse_string_to_json_value_vec; +use crate::{api::fetch_functions, components::button::button::Button}; use leptos::*; use serde_json::{json, Value}; use std::str::FromStr; @@ -14,6 +18,7 @@ pub fn dimension_form<NF>( #[prop(default = String::new())] dimension_name: String, #[prop(default = String::new())] dimension_type: String, #[prop(default = String::new())] dimension_pattern: String, + #[prop(default = None)] function_name: Option<Value>, handle_submit: NF, ) -> impl IntoView where @@ -26,6 +31,31 @@ where let (dimension_type, set_dimension_type) = create_signal(dimension_type); let (dimension_pattern, set_dimension_pattern) = create_signal(dimension_pattern); + let (function_name, set_function_name) = create_signal(function_name); + + let functions_resource: Resource<String, Vec<crate::types::FunctionResponse>> = + create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_functions(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let handle_select_dropdown_option = move |selected_function: FunctionsName| { + set_function_name.update(|value| { + let function_name = selected_function.clone(); + leptos::logging::log!("function selected: {:?}", function_name); + let fun_name = match function_name.as_str() { + "None" => None, + _ => Some(json!(function_name)), + }; + *value = fun_name; + }); + }; + let (show_labels, set_show_labels) = create_signal(edit); let (error_message, set_error_message) = create_signal("".to_string()); @@ -36,6 +66,7 @@ where let f_name = dimension_name.get(); let f_type = dimension_type.get(); let f_pattern = dimension_pattern.get(); + let fun_name = function_name.get(); let f_schema = match f_type.as_str() { "number" => { @@ -73,6 +104,7 @@ where dimension: f_name, priority: f_priority, schema: f_schema, + function_name: fun_name, }; let handle_submit_clone = handle_submit.clone(); @@ -97,14 +129,14 @@ where view! { <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Dimension</span> + <label class="label"> + <span class="label-text">Dimension</span> </label> <input disabled=edit type="text" placeholder="Dimension" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md" value=dimension_name.get() on:change=move |ev| { let value = event_target_value(&ev); @@ -113,6 +145,13 @@ where /> </div> + + <div class="divider"></div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Set Schema</span> + </label> <select name="schemaType[]" on:change=move |ev| { @@ -143,7 +182,7 @@ where }; } - class="select select-bordered" + class="select select-bordered w-full max-w-md" > <option disabled selected> Set Schema @@ -187,17 +226,20 @@ where "Other" </option> </select> + </div> + + <div class="divider"></div> {move || { view! { <div class="form-control"> - <label class="label font-mono"> - <span class="label-text text-gray-700 font-mono">Priority</span> + <label class="label"> + <span class="label-text">Priority</span> </label> <input type="Number" placeholder="Priority" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md" value=priority.get() on:change=move |ev| { logging::log!( @@ -221,7 +263,7 @@ where </label> <textarea type="text" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" + class="input input-bordered w-full max-w-md pt-[10px]" on:change=move |ev| { let value = event_target_value(&ev); logging::log!("{:?}", value); @@ -233,12 +275,49 @@ where </textarea> </div> + <div class="divider"></div> </Show> } }} - <div class="form-control mt-6"> - <Button text="Submit".to_string() on_click=on_submit/> + <Suspense> + {move || { + let mut functions = functions_resource.get().unwrap_or(vec![]); + let mut function_names: Vec<FunctionsName> = vec!["None".to_string()]; + functions.sort_by(|a, b| a.function_name.cmp(&b.function_name)); + functions.into_iter().for_each(|ele| { + function_names.push(ele.function_name); + }); + view! { + <div class="form-control"> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> + <span class="label-text">Function Name</span> + <span class="label-text text-slate-400">Assign Function validation to your key</span> + </label> + </div> + + <div class="mt-2"> + <Dropdown + dropdown_width="w-100" + dropdown_icon="".to_string() + dropdown_text={function_name.get().and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }).map_or("Add Function".to_string(), |v| v.to_string())} + dropdown_direction=DropdownDirection::Down + dropdown_btn_type=DropdownBtnType::Select + dropdown_options=function_names + on_select=Box::new(handle_select_dropdown_option) + /> + </ div> + </ div> + } + }} + </ Suspense> + + <div class="form-control grid w-full justify-end"> + <Button class="pl-[70px] pr-[70px]".to_string() text="Submit".to_string() on_click=on_submit/> </div> { diff --git a/crates/frontend/src/components/dimension_form/types.rs b/crates/frontend/src/components/dimension_form/types.rs index ecf60201a..e4e60e1ac 100644 --- a/crates/frontend/src/components/dimension_form/types.rs +++ b/crates/frontend/src/components/dimension_form/types.rs @@ -6,4 +6,5 @@ pub struct DimensionCreateReq { pub dimension: String, pub priority: u16, pub schema: Value, + pub function_name: Option<Value>, } diff --git a/crates/frontend/src/components/drawer/drawer.rs b/crates/frontend/src/components/drawer/drawer.rs index f7dd45e8a..a4be54c54 100644 --- a/crates/frontend/src/components/drawer/drawer.rs +++ b/crates/frontend/src/components/drawer/drawer.rs @@ -38,20 +38,26 @@ pub fn drawer_btn(drawer_id: String, children: Children) -> impl IntoView { } #[component] -pub fn drawer( +pub fn drawer<NF>( id: String, children: Children, #[prop(default = "")] header: &'static str, #[prop(default = "w-[60vw]")] drawer_width: &'static str, -) -> impl IntoView { - let close_drawer_id = id.clone(); + handle_close: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let close_drawer = move |_| { + handle_close(); + }; view! { <div class="drawer drawer-end"> <input id=id.clone() type="checkbox" class="drawer-toggle"/> <div class="drawer-side drawer-zindex w-full"> - <label for=id.clone() class="drawer-overlay"></label> + <label for=id.clone() class="drawer-overlay" on:click=close_drawer.clone()></label> <div class=format!( "min-h-full {drawer_width} bg-base-100 overflow-x-hidden overflow-y-auto", )> @@ -59,7 +65,7 @@ pub fn drawer( <h3 class="text-lg font-bold">{header}</h3> <button class="btn btn-sm btn-circle btn-ghost" - on:click=move |_| { close_drawer(&close_drawer_id) } + on:click=close_drawer > <i class="ri-close-line"></i> </button> diff --git a/crates/frontend/src/components/dropdown/dropdown.rs b/crates/frontend/src/components/dropdown/dropdown.rs index 52d03253d..0bb733b52 100644 --- a/crates/frontend/src/components/dropdown/dropdown.rs +++ b/crates/frontend/src/components/dropdown/dropdown.rs @@ -7,20 +7,23 @@ pub enum DropdownBtnType { Outline, Link, Fill, + Select, } #[derive(PartialEq, Copy, Clone)] pub enum DropdownDirection { Right, Left, + Top, + Down, } #[component] pub fn dropdown<T>( dropdown_text: String, - dropdown_icon: String, dropdown_options: Vec<T>, on_select: Box<dyn Fn(T)>, + #[prop(default = "".to_string())] dropdown_icon: String, #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, #[prop(default = DropdownBtnType::Outline)] dropdown_btn_type: DropdownBtnType, #[prop(default = "w-96")] dropdown_width: &'static str, @@ -42,19 +45,23 @@ where }); let on_select = StoredValue::new(on_select); + let btn_class = match dropdown_btn_type { + DropdownBtnType::Outline => "btn btn-sm text-xs m-1 w-full btn-purple-outline", + DropdownBtnType::Link => "btn btn-sm text-xs m-1 w-full btn-purple-link", + DropdownBtnType::Fill => "btn btn-sm text-xs m-1 w-full btn-purple-fill", + DropdownBtnType::Select => "select select-bordered w-[28rem] items-center", + }; + view! { <div class="dropdown" class=("disable-click", disabled) class=("dropdown-right", dropdown_direction == DropdownDirection::Right) class=("dropdown-left", dropdown_direction == DropdownDirection::Left) + class=("dropdown-top", dropdown_direction == DropdownDirection::Top) + class=("dropdown-down", dropdown_direction == DropdownDirection::Down) > - <label - tabindex="0" - class="btn btn-sm text-xs m-1 w-full" - class=("btn-purple-outline", dropdown_btn_type == DropdownBtnType::Outline) - class=("btn-purple-link", dropdown_btn_type == DropdownBtnType::Link) - > + <label tabindex="0" class=btn_class> <i class=format!("{dropdown_icon}")></i> {dropdown_text} </label> diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs index 47e8a1f79..ad2d1b28b 100644 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -1,15 +1,11 @@ -use std::collections::HashMap; - -use crate::components::default_config_form::default_config_form::DefaultConfigForm; -use crate::components::modal::modal::Modal; -use crate::components::table::{table::Table, types::Column}; - use crate::api::fetch_default_config; -use crate::components::button::button::Button; +use crate::components::default_config_form::default_config_form::DefaultConfigForm; +use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; use crate::components::stat::stat::Stat; -use crate::utils::{close_modal, show_modal}; +use crate::components::table::{table::Table, types::Column}; use leptos::*; use serde_json::{json, Map, Value}; +use std::collections::HashMap; #[derive(Clone, Debug, Default)] pub struct RowData { @@ -17,6 +13,7 @@ pub struct RowData { pub value: String, pub pattern: String, pub type_: String, + pub function_name: Option<Value>, } #[component] @@ -44,6 +41,12 @@ pub fn DefaultConfig() -> impl IntoView { let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) .unwrap_or(HashMap::new()); + let function_name = row["function_name"].clone().to_string(); + let fun_name = match function_name.as_str() { + "null" => None, + _ => Some(json!(function_name.replace("\"", ""))), + }; + let pattern_or_enum = schema_object .keys() .find(|key| { @@ -103,23 +106,23 @@ pub fn DefaultConfig() -> impl IntoView { value: row_value.clone(), type_: row_type.clone(), pattern: row_pattern.clone(), + function_name: fun_name.clone(), }; - logging::log!("{:?}", row_data); - selected_config.set(Some(row_data)); - show_modal("default_config_modal_form"); + open_drawer("default_config_drawer"); }; let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; - view! { <span on:click=edit_click_handler>{edit_icon}</span> }.into_view() + view! { <span class="cursor-pointer" on:click=edit_click_handler>{edit_icon}</span> }.into_view() }; vec![ Column::default("key".to_string()), Column::default("schema".to_string()), Column::default("value".to_string()), + Column::default("function_name".to_string()), Column::default("created_at".to_string()), Column::default("created_by".to_string()), Column::new("EDIT".to_string(), None, edit_col_formatter), @@ -128,45 +131,44 @@ pub fn DefaultConfig() -> impl IntoView { view! { <div class="p-8"> - <Modal - id="default_config_modal_form".to_string() - handle_close=move || { - close_modal("default_config_modal_form"); + {move || { + let handle_close = move || { + close_drawer("default_config_drawer"); selected_config.set(None); - } - > - - {move || { - if let Some(selected_config_data) = selected_config.get() { - view! { - <DefaultConfigForm - edit=true - config_key=selected_config_data.key - config_value=selected_config_data.value - config_type=selected_config_data.type_ - config_pattern=selected_config_data.pattern - handle_submit=move || { - default_config_resource.refetch(); - close_modal("default_config_modal_form"); - selected_config.set(None); - } - /> - } - } else { - view! { + }; + if let Some(selected_config_data) = selected_config.get() { + view! { + <Drawer id="default_config_drawer".to_string() header="Edit Key" handle_close=handle_close> + <DefaultConfigForm + edit=true + config_key=selected_config_data.key + config_value=selected_config_data.value + config_type=selected_config_data.type_ + config_pattern=selected_config_data.pattern + function_name=selected_config_data.function_name + handle_submit=move || { + default_config_resource.refetch(); + close_drawer("default_config_drawer"); + selected_config.set(None); + } + /> + </Drawer> + } + } else { + view! { + <Drawer id="default_config_drawer".to_string() header="Create New Key" handle_close=handle_close> <DefaultConfigForm handle_submit=move || { default_config_resource.refetch(); - close_modal("default_config_modal_form"); + close_drawer("default_config_drawer"); }/> - } + </Drawer> } - }} + } + }} - </Modal> <Suspense fallback=move || { view! { <p>"Loading (Suspense Fallback)..."</p> } }> - {move || { let default_config = default_config_resource.get().unwrap_or(vec![]); let total_default_config_keys = default_config.len().to_string(); @@ -184,11 +186,7 @@ pub fn DefaultConfig() -> impl IntoView { .collect::<Vec<Map<String, Value>>>(); view! { <div class="pb-4"> - <Stat - heading="Config Keys" - icon="ri-tools-line" - number=total_default_config_keys - /> + <Stat heading="Config Keys" icon="ri-tools-line" number=total_default_config_keys/> </div> <div class="card rounded-lg w-full bg-base-100 shadow"> <div class="card-body"> @@ -196,13 +194,9 @@ pub fn DefaultConfig() -> impl IntoView { <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> "Default Config" </h2> - <Button - text="Create Key".to_string() - on_click=move |_| { - show_modal("default_config_modal_form") - } - /> - + <DrawerBtn drawer_id="default_config_drawer".to_string()> + Create Key <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> </div> <Table cell_style="min-w-48 font-mono".to_string() @@ -214,7 +208,6 @@ pub fn DefaultConfig() -> impl IntoView { </div> } }} - </Suspense> </div> } diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs index da89d7eef..1928d9e54 100644 --- a/crates/frontend/src/pages/Dimensions/Dimensions.rs +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -1,15 +1,12 @@ -use std::collections::HashMap; - -use crate::components::button::button::Button; use crate::components::dimension_form::dimension_form::DimensionForm; -use crate::components::modal::modal::Modal; +use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; use crate::components::{ stat::stat::Stat, table::{table::Table, types::Column}, }; -use crate::utils::{close_modal, show_modal}; use leptos::*; use serde_json::{json, Map, Value}; +use std::collections::HashMap; use crate::api::fetch_dimensions; @@ -19,14 +16,13 @@ pub struct RowData { pub priority: u16, pub type_: String, pub pattern: String, + pub function_name: Option<Value>, } #[component] pub fn Dimensions() -> impl IntoView { let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); - let (open_form, set_open_form) = create_signal(false); - - let dimensions = create_blocking_resource( + let dimensions_resource = create_blocking_resource( move || tenant_rs.get(), |current_tenant| async move { match fetch_dimensions(current_tenant).await { @@ -40,6 +36,7 @@ pub fn Dimensions() -> impl IntoView { let table_columns = create_memo(move |_| { let edit_col_formatter = move |_: &str, row: &Map<String, Value>| { + logging::log!("Dimension row: {:?}", row); let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); let row_priority_str = row["priority"].clone().to_string().replace("\"", ""); let row_priority = match row_priority_str.parse::<u16>() { @@ -51,6 +48,12 @@ pub fn Dimensions() -> impl IntoView { let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) .unwrap_or(HashMap::new()); + let function_name = row["function_name"].clone().to_string(); + let fun_name = match function_name.as_str() { + "null" => None, + _ => Some(json!(function_name.replace("\"", ""))), + }; + let pattern_or_enum = schema_object .keys() .find(|key| { @@ -110,20 +113,23 @@ pub fn Dimensions() -> impl IntoView { priority: row_priority.clone(), type_: row_type.clone(), pattern: row_pattern.clone(), + function_name: fun_name.clone(), }; + logging::log!("{:?}", row_data); selected_dimension.set(Some(row_data)); - set_open_form.set(true); - show_modal("dimension_form_modal"); + open_drawer("dimension_drawer"); }; - let edit_icon: HtmlElement<html::I> = view! { <i class="ri-pencil-line ri-xl text-blue-500" on:click=edit_click_handler></i> }; + let edit_icon: HtmlElement<html::I> = + view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; - view! { <span class="cursor-pointer">{edit_icon}</span> }.into_view() + view! { <span class="cursor-pointer" on:click=edit_click_handler>{edit_icon}</span> }.into_view() }; vec![ Column::default("dimension".to_string()), Column::default("priority".to_string()), Column::default("schema".to_string()), + Column::default("function_name".to_string()), Column::default("created_by".to_string()), Column::default("created_at".to_string()), Column::new("EDIT".to_string(), None, edit_col_formatter), @@ -132,105 +138,83 @@ pub fn Dimensions() -> impl IntoView { view! { <div class="p-8"> - <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> - <div class="pb-4"> - {move || { - let value = dimensions.get(); - let total_items = match value { - Some(v) => v.len().to_string(), - _ => "0".to_string(), - }; - view! { - <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> - } - }} - <Show when=move || { open_form.get() }> - <Modal - id="dimension_form_modal".to_string() - handle_close=move || { - close_modal("dimension_form_modal"); + {move || { + let handle_close = move || { + close_drawer("dimension_drawer"); + selected_dimension.set(None); + }; + if let Some(selected_dimension_data) = selected_dimension.get() { + view! { + <Drawer id="dimension_drawer".to_string() header="Edit Dimension" handle_close=handle_close> + <DimensionForm + edit=true + priority=selected_dimension_data.priority + dimension_name=selected_dimension_data.dimension + dimension_type=selected_dimension_data.type_ + dimension_pattern=selected_dimension_data.pattern + function_name=selected_dimension_data.function_name + handle_submit=move || { + dimensions_resource.refetch(); selected_dimension.set(None); + close_drawer("dimension_drawer"); } - > - - {move || { - if let Some(selected_dimension_data) = selected_dimension.get() { - view! { - <DimensionForm - edit=true - priority=selected_dimension_data.priority - dimension_name=selected_dimension_data.dimension - dimension_type=selected_dimension_data.type_ - dimension_pattern=selected_dimension_data.pattern - handle_submit=move || { - set_open_form.set(false); - dimensions.refetch(); - selected_dimension.set(None); - } - /> - } - } else { - view! { - <DimensionForm handle_submit=move || { - set_open_form.set(false); - dimensions.refetch() - }/> - } - } - }} - - </Modal> - </Show> - </div> - - <div class="card rounded-xl w-full bg-base-100 shadow"> - <div class="card-body"> - <div class="flex justify-between mb-2"> - <h2 class="card-title">Dimensions</h2> - <Button - text="Create Dimension".to_string() - on_click=move |_| { - set_open_form.set(true); - show_modal("dimension_form_modal"); - } - /> - + /> + </Drawer> + } + } else { + view! { + <Drawer id="dimension_drawer".to_string() header="Create New Dimension" handle_close=handle_close> + <DimensionForm handle_submit=move || { + dimensions_resource.refetch(); + close_drawer("dimension_drawer"); + }/> + </Drawer> + } + } + }} + + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)...."</p> } + }> + {move || { + let value = dimensions_resource.get().unwrap_or(vec![]); + let total_items = value.len().to_string(); + let table_rows = value + .iter() + .map(|ele| { + let mut ele_map = json!(ele).as_object().unwrap().clone(); + ele_map + .insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()), + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> </div> - <div> - - {move || { - let value = dimensions.get(); - match value { - Some(v) => { - let data = v - .iter() - .map(|ele| { - let mut ele_map = json!(ele).as_object().unwrap().clone(); - ele_map - .insert( - "created_at".to_string(), - json!(ele.created_at.format("%v").to_string()), - ); - ele_map - }) - .collect::<Vec<Map<String, Value>>>() - .to_owned(); - view! { - <Table - cell_style="min-w-48 font-mono".to_string() - rows=data - key_column="id".to_string() - columns=table_columns.get() - /> - } - } - None => view! { <div>Loading....</div> }.into_view(), - } - }} - + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Dimensions" + </h2> + <DrawerBtn drawer_id="dimension_drawer".to_string()> + Create Dimension <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> + </div> + <Table + cell_style="min-w-48 font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + /> + </div> </div> - </div> - </div> + } + }} </Suspense> </div> } diff --git a/crates/frontend/src/pages/experiment_list/experiment_list.rs b/crates/frontend/src/pages/experiment_list/experiment_list.rs index 4f16411da..4af09fd6a 100644 --- a/crates/frontend/src/pages/experiment_list/experiment_list.rs +++ b/crates/frontend/src/pages/experiment_list/experiment_list.rs @@ -224,7 +224,9 @@ pub fn experiment_list() -> impl IntoView { .default_config; let _ = reset_exp_form.get(); view! { - <Drawer id="create_exp_drawer".to_string() header="Create New Experiment"> + <Drawer id="create_exp_drawer".to_string() header="Create New Experiment" handle_close=move || { + close_drawer("create_exp_drawer"); + }> <ExperimentForm name="".to_string() context=vec![] diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index 84ca4e96b..b47a25567 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -151,6 +151,7 @@ pub struct Dimension { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, + pub function_name: Option<String>, } impl DropdownOption for Dimension { @@ -169,6 +170,7 @@ pub struct DefaultConfig { pub created_at: DateTime<Utc>, pub created_by: String, pub schema: Value, + pub function_name: Option<String>, } impl DropdownOption for DefaultConfig { @@ -193,3 +195,13 @@ pub struct Config { pub overrides: Map<String, Value>, pub default_configs: Map<String, Value>, } + +pub type FunctionsName = String; +impl DropdownOption for FunctionsName { + fn key(&self) -> String { + self.clone() + } + fn label(&self) -> String { + self.clone() + } +} From 9208b3e52841216674599f36d3b0b3270f3c9624 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 10 Apr 2024 10:08:01 +0000 Subject: [PATCH 339/352] chore(version): v0.38.0 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/frontend/CHANGELOG.md | 8 ++++++++ crates/frontend/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de7e01c44..cfc48337c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.38.0 - 2024-04-10 +### Package updates +- frontend bumped to frontend-v0.5.0 +### Global changes + +- - - + ## v0.37.0 - 2024-04-10 ### Package updates - service-utils bumped to service-utils-v0.13.0 diff --git a/Cargo.lock b/Cargo.lock index 609726321..c3df06542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix-files", "actix-web", diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index 16b44c826..d7ea83742 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.5.0 - 2024-04-10 +#### Features +- PICAF-26529 Add function support in dimension and default config form - (6a942fe) - ankit.mahato +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + ## frontend-v0.4.0 - 2024-04-05 #### Features - [PICAF-26360] added decimal support in context and override form and fixed dimension modal - (3f1f998) - Saurav Suman diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index cb198bd8e..9c231ef2f 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.4.0" +version = "0.5.0" edition = "2021" [lib] From 42881c8ba6dcad44f54dc9e89be284ba99fdd6c6 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Wed, 10 Apr 2024 16:30:59 +0530 Subject: [PATCH 340/352] fix: added service-prefix to functions endpoints --- crates/context-aware-config/src/auth.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/context-aware-config/src/auth.rs b/crates/context-aware-config/src/auth.rs index e2b2a826f..e58c2f152 100644 --- a/crates/context-aware-config/src/auth.rs +++ b/crates/context-aware-config/src/auth.rs @@ -140,49 +140,49 @@ pub mod functions { pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { Vec::from([ ( - "POST::/function", + "POST::{service_prefix}/function", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PATCH::/function/{function_name}", + "PATCH::{service_prefix}/function/{function_name}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "GET::/function/{function_name}", + "GET::{service_prefix}/function/{function_name}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "DELETE::/function/{function_name}", + "DELETE::{service_prefix}/function/{function_name}", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "GET::/function", + "GET::{service_prefix}/function", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/function/{function_name}/{stage}/test", + "PUT::{service_prefix}/function/{function_name}/{stage}/test", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), }, ), ( - "PUT::/function/{function_name}/publish", + "PUT::{service_prefix}/function/{function_name}/publish", AuthenticatedRoute { api_tag: "MANAGER".into(), user_permissions: ("manager".into(), "RW".into()), From ea982bf30856af3ccdbb791fd9281a060a245604 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 10 Apr 2024 11:54:24 +0000 Subject: [PATCH 341/352] chore(version): v0.38.1 [skip ci] --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- crates/context-aware-config/CHANGELOG.md | 8 ++++++++ crates/context-aware-config/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc48337c..28bf14fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.38.1 - 2024-04-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.2 +### Global changes + +- - - + ## v0.38.0 - 2024-04-10 ### Package updates - frontend bumped to frontend-v0.5.0 diff --git a/Cargo.lock b/Cargo.lock index c3df06542..dbd6413b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,7 +900,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.25.1" +version = "0.25.2" dependencies = [ "actix", "actix-cors", diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 4644dfa3b..315e86694 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.25.2 - 2024-04-10 +#### Bug Fixes +- PICAF-26366 added service-prefix to functions endpoints - (5492072) - ankit.mahato +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + ## context-aware-config-v0.25.1 - 2024-04-08 #### Bug Fixes - [PICAF-26346] add path to node_modules - (c4bc7b6) - Pratik Mishra diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index f554441a5..507e189eb 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.25.1" +version = "0.25.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 6735635082bb6eb9cbf3257f925b1eeebad800fc Mon Sep 17 00:00:00 2001 From: Pratik Mishra <pratik.mishra@juspay.in> Date: Fri, 12 Apr 2024 15:03:20 +0530 Subject: [PATCH 342/352] fix: function route fix --- .env.example | 2 +- .../src/components/default_config_form/utils.rs | 10 +++++++--- crates/frontend/src/pages/function/function_create.rs | 4 +--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 1aad4bcc2..7820179ae 100644 --- a/.env.example +++ b/.env.example @@ -26,5 +26,5 @@ TENANT_VALIDATION_ENABLED=false TENANTS=dev,test TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" -SERVICE_PREFIX="" +SERVICE_PREFIX="superposition" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/crates/frontend/src/components/default_config_form/utils.rs b/crates/frontend/src/components/default_config_form/utils.rs index fdefab3eb..32f492913 100644 --- a/crates/frontend/src/components/default_config_form/utils.rs +++ b/crates/frontend/src/components/default_config_form/utils.rs @@ -19,9 +19,13 @@ pub async fn create_default_config( .await .map_err(|e| e.to_string())?; match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + StatusCode::OK | StatusCode::CREATED => { + response.text().await.map_err(|e| e.to_string()) + } + StatusCode::BAD_REQUEST => Err(response + .text() + .await + .unwrap_or("Validation of configuration value failed, but the error could not be understood by the system. Contact an admin for help if this persists".to_string())), _ => Err("Internal Server Error".to_string()), } } diff --git a/crates/frontend/src/pages/function/function_create.rs b/crates/frontend/src/pages/function/function_create.rs index 0e3f677eb..937330cc9 100644 --- a/crates/frontend/src/pages/function/function_create.rs +++ b/crates/frontend/src/pages/function/function_create.rs @@ -1,6 +1,5 @@ use crate::components::function_form::function_form::FunctionEditor; use crate::types::FunctionResponse; -use crate::utils::use_url_base; use leptos::*; use leptos_router::use_navigate; use serde::{Deserialize, Serialize}; @@ -48,8 +47,7 @@ pub fn create_function_view() -> impl IntoView { edit=false handle_submit=move || { let tenant = tenant_rs.get(); - let base = use_url_base(); - let redirect_url = format!("{base}/admin/{tenant}/function"); + let redirect_url = format!("admin/{tenant}/function"); let navigate = use_navigate(); navigate(redirect_url.as_str(), Default::default()) } From 64560fbad51330de01929a9f5ac4e0119a05aaf3 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Fri, 12 Apr 2024 10:25:33 +0000 Subject: [PATCH 343/352] chore(version): v0.38.2 [skip ci] --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- crates/frontend/CHANGELOG.md | 6 ++++++ crates/frontend/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28bf14fc3..045ef4e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.38.2 - 2024-04-12 +### Package updates +- frontend bumped to frontend-v0.5.1 +### Global changes +#### Bug Fixes +- [PICAF-26529] function route fix - (aba54da) - Pratik Mishra + +- - - + ## v0.38.1 - 2024-04-10 ### Package updates - context-aware-config bumped to context-aware-config-v0.25.2 diff --git a/Cargo.lock b/Cargo.lock index dbd6413b5..7c1b143fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,7 @@ dependencies = [ [[package]] name = "frontend" -version = "0.5.0" +version = "0.5.1" dependencies = [ "actix-files", "actix-web", diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md index d7ea83742..13dcf4adc 100644 --- a/crates/frontend/CHANGELOG.md +++ b/crates/frontend/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## frontend-v0.5.1 - 2024-04-12 +#### Bug Fixes +- [PICAF-26529] function route fix - (aba54da) - Pratik Mishra + +- - - + ## frontend-v0.5.0 - 2024-04-10 #### Features - PICAF-26529 Add function support in dimension and default config form - (6a942fe) - ankit.mahato diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 9c231ef2f..36ed1e2a7 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frontend" -version = "0.5.0" +version = "0.5.1" edition = "2021" [lib] From 0cc2e6dfdd36198fd7948beda66c9882de95268b Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Mon, 8 Apr 2024 18:33:08 +0530 Subject: [PATCH 344/352] feat: Add filter support to client --- Cargo.lock | 1 + clients/haskell/hs-cac-client/src/Client.hs | 35 ++-- clients/haskell/hs-cac-client/src/Main.hs | 12 +- crates/cac_client/Cargo.toml | 1 + crates/cac_client/src/interface.rs | 79 +++++--- crates/cac_client/src/lib.rs | 176 +++++++++++++++--- .../src/api/config/helpers.rs | 8 +- docs/client-context-aware-configuration.md | 60 +++--- headers/libcac_client.h | 6 +- 9 files changed, 276 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c1b143fd..9838cc728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "service-utils", "strum", "strum_macros", "tokio", diff --git a/clients/haskell/hs-cac-client/src/Client.hs b/clients/haskell/hs-cac-client/src/Client.hs index e0e4cde62..a5fe740bd 100644 --- a/clients/haskell/hs-cac-client/src/Client.hs +++ b/clients/haskell/hs-cac-client/src/Client.hs @@ -6,7 +6,7 @@ module Client ( createCacClient , getCacClient -, getFullConfigState +, getFullConfigStateWithFilter , getCacLastModified , getResolvedConfig , cacStartPolling @@ -50,7 +50,7 @@ foreign import ccall unsafe "get_last_modified" c_get_last_modified_time :: Ptr CacClient -> IO CString foreign import ccall unsafe "get_config" - c_get_config :: Ptr CacClient -> IO CString + c_get_config :: Ptr CacClient -> CString -> IO CString foreign import ccall unsafe "get_resolved_config" c_cac_get_resolved_config :: Ptr CacClient -> CString -> CString -> CString -> IO CString @@ -100,9 +100,13 @@ getCacClient tenant = do then Left <$> getError else Right <$> newForeignPtr c_free_cac_client cacClient -getFullConfigState :: ForeignPtr CacClient -> IO (Either Error Value) -getFullConfigState client = do - config <- withForeignPtr client c_get_config +getFullConfigStateWithFilter :: ForeignPtr CacClient -> Maybe String -> IO (Either Error Value) +getFullConfigStateWithFilter client mbFilters = do + cFilters <- case mbFilters of + Just filters -> newCAString filters + Nothing -> return nullPtr + config <- withForeignPtr client (`c_get_config` cFilters) + _ <- cleanup [cFilters] if config == nullPtr then Left <$> getError else do @@ -118,11 +122,13 @@ getCacLastModified client = do fptrLastModified <- newForeignPtr c_free_string lastModified Right <$> withForeignPtr fptrLastModified peekCAString -getResolvedConfigWithStrategy :: ForeignPtr CacClient -> String -> [String] -> MergeStrategy -> IO (Either Error Value) -getResolvedConfigWithStrategy client context keys mergeStrat = do +getResolvedConfigWithStrategy :: ForeignPtr CacClient -> String -> Maybe [String] -> MergeStrategy -> IO (Either Error Value) +getResolvedConfigWithStrategy client context mbKeys mergeStrat = do cContext <- newCAString context cMergeStrat <- newCAString (show mergeStrat) - cStrKeys <- newCAString (intercalate "|" keys) + cStrKeys <- case mbKeys of + Just keys -> newCAString (intercalate "|" keys) + Nothing -> return nullPtr overrides <- withForeignPtr client $ \client -> c_cac_get_resolved_config client cContext cStrKeys cMergeStrat _ <- cleanup [cContext, cStrKeys] if overrides == nullPtr @@ -131,9 +137,11 @@ getResolvedConfigWithStrategy client context keys mergeStrat = do fptrOverrides <- newForeignPtr c_free_string overrides Right . toJSON <$> withForeignPtr fptrOverrides peekCAString -getDefaultConfig :: ForeignPtr CacClient -> [String] -> IO (Either Error Value) -getDefaultConfig client keys = do - cStrKeys <- newCAString (intercalate "|" keys) +getDefaultConfig :: ForeignPtr CacClient -> Maybe [String] -> IO (Either Error Value) +getDefaultConfig client mbKeys = do + cStrKeys <- case mbKeys of + Just keys -> newCAString (intercalate "|" keys) + Nothing -> return nullPtr overrides <- withForeignPtr client $ \client -> c_cac_get_default_config client cStrKeys _ <- cleanup [cStrKeys] if overrides == nullPtr @@ -142,5 +150,6 @@ getDefaultConfig client keys = do fptrOverrides <- newForeignPtr c_free_string overrides Right . toJSON <$> withForeignPtr fptrOverrides peekCAString -getResolvedConfig :: ForeignPtr CacClient -> String -> [String] -> IO (Either Error Value) -getResolvedConfig client context keys = getResolvedConfigWithStrategy client context keys MERGE +getResolvedConfig :: ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error Value) +getResolvedConfig client context mbKeys = getResolvedConfigWithStrategy client context mbKeys MERGE + diff --git a/clients/haskell/hs-cac-client/src/Main.hs b/clients/haskell/hs-cac-client/src/Main.hs index 81bea9ee4..9ad034b8a 100644 --- a/clients/haskell/hs-cac-client/src/Main.hs +++ b/clients/haskell/hs-cac-client/src/Main.hs @@ -2,7 +2,7 @@ module Main (main) where import Client (getResolvedConfig, createCacClient, getCacClient, - getFullConfigState, getCacLastModified, cacStartPolling, getDefaultConfig) + getFullConfigStateWithFilter, getCacLastModified, cacStartPolling, getDefaultConfig) import Control.Concurrent import Prelude @@ -16,13 +16,15 @@ main = do getCacClient "dev" >>= \case Left err -> putStrLn err Right client -> do - config <- getFullConfigState client - lastModified <- getCacLastModified client - overrides <- getResolvedConfig client "{\"country\": \"India\"}" ["country_image_url", "hyperpay_version"] - defaults <- getDefaultConfig client ["country_image_url", "hyperpay_version"] + config <- getFullConfigStateWithFilter client Nothing + lastModified <- getCacLastModified client + overrides <- getResolvedConfig client "{\"country\": \"India\"}" $ Just ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client $ Just ["country_image_url", "hyperpay_version"] + filteredConfig <- getFullConfigStateWithFilter client $ Just "{\"prefix\": \"hyperpay\", \"os\": \"android\"}" print config print lastModified print overrides print defaults + print filteredConfig threadDelay 1000000000 pure () diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index c08c40b9f..0c8781616 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -19,6 +19,7 @@ log = { workspace = true } strum_macros = { workspace = true } strum = { workspace = true } tokio = {version = "1.29.1", features = ["full"]} +service-utils = { path = "../service-utils" } [lib] name = "cac_client" diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs index f8e4f12c3..55d2efd59 100644 --- a/crates/cac_client/src/interface.rs +++ b/crates/cac_client/src/interface.rs @@ -176,7 +176,31 @@ pub extern "C" fn get_last_modified(client: *mut Arc<Client>) -> *const c_char { } #[no_mangle] -pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { +pub extern "C" fn get_config( + client: *mut Arc<Client>, + query: *const c_char, +) -> *const c_char { + let filter = if query.is_null() { + None + } else { + let filter_string = match cstring_to_rstring(query) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + let filters: Map<String, Value> = + match serde_json::from_str::<Map<String, Value>>(filter_string.as_str()) { + Ok(json) => json, + Err(err) => { + update_last_error(err.to_string()); + return std::ptr::null(); + } + }; + Some(filters) + }; + null_check!( client, "an invalid null pointer client is being used, please call get_client()", @@ -184,10 +208,12 @@ pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { ); unwrap_safe!( unsafe { - (*client).get_full_config_state().map(|config| { - rstring_to_cstring(serde_json::to_value(config).unwrap().to_string()) - .into_raw() - }) + (*client) + .get_full_config_state_with_filter(filter) + .map(|config| { + rstring_to_cstring(serde_json::to_value(config).unwrap().to_string()) + .into_raw() + }) }, return std::ptr::null_mut() ) @@ -197,7 +223,7 @@ pub extern "C" fn get_config(client: *mut Arc<Client>) -> *const c_char { pub extern "C" fn get_resolved_config( client: *mut Arc<Client>, query: *const c_char, - keys: *const c_char, + filter_keys: *const c_char, merge_strategy: *const c_char, ) -> *const c_char { null_check!( @@ -206,11 +232,17 @@ pub extern "C" fn get_resolved_config( return std::ptr::null() ); - let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); - let key_vector = if key.is_empty() { - vec![] + let keys: Option<Vec<String>> = if filter_keys.is_null() { + None } else { - key.split("|").map(str::to_string).collect() + let filter_string = match cstring_to_rstring(filter_keys) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + Some(filter_string.split("|").map(str::to_string).collect()) }; let query = unwrap_safe!(cstring_to_rstring(query), return std::ptr::null()); @@ -218,7 +250,7 @@ pub extern "C" fn get_resolved_config( unwrap_safe!(cstring_to_rstring(merge_strategy), return std::ptr::null()); println!( "key vector {:#?}, merge strategy {:#?}", - key_vector, merge_strategem + keys, merge_strategem ); let context = unwrap_safe!( @@ -229,11 +261,7 @@ pub extern "C" fn get_resolved_config( unwrap_safe!( unsafe { (*client) - .get_resolved_config( - context, - key_vector, - MergeStrategy::from(merge_strategem), - ) + .get_resolved_config(context, keys, MergeStrategy::from(merge_strategem)) .map(|ov| { unwrap_safe!( serde_json::to_string::<Map<String, Value>>(&ov) @@ -249,17 +277,24 @@ pub extern "C" fn get_resolved_config( #[no_mangle] pub extern "C" fn get_default_config( client: *mut Arc<Client>, - keys: *const c_char, + filter_keys: *const c_char, ) -> *const c_char { - let key = unwrap_safe!(cstring_to_rstring(keys), return std::ptr::null()); - let key_vector = if key.is_empty() { - vec![] + let keys: Option<Vec<String>> = if filter_keys.is_null() { + None } else { - key.split("|").map(str::to_string).collect() + let filter_string = match cstring_to_rstring(filter_keys) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + Some(filter_string.split("|").map(str::to_string).collect()) }; + unwrap_safe!( unsafe { - (*client).get_default_config(key_vector).map(|ov| { + (*client).get_default_config(keys).map(|ov| { unwrap_safe!( serde_json::to_string::<Map<String, Value>>(&ov) .map(|overrides| rstring_to_cstring(overrides).into_raw()), diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 809363da9..75bbabc7a 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -9,7 +9,7 @@ use reqwest::{RequestBuilder, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, convert::identity, sync::{Arc, RwLock}, time::{Duration, UNIX_EPOCH}, @@ -17,6 +17,10 @@ use std::{ use strum_macros; use utils::core::MapError; +use service_utils::{ + errors::types::Error as err, helpers::extract_dimensions, types as app, +}; + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { pub condition: Value, @@ -151,8 +155,34 @@ impl Client { } } - pub fn get_full_config_state(&self) -> Result<Config, String> { - self.config.read().map(|c| c.clone()).map_err_to_string() + pub fn get_full_config_state_with_filter( + &self, + query_data: Option<Map<String, Value>>, + ) -> Result<Config, String> { + let mut config = self.config.read().map(|c| c.clone()).map_err_to_string()?; + if let Some(mut query_map) = query_data { + if let Some(prefix) = query_map.get("prefix") { + let prefix_list: HashSet<&str> = prefix + .as_str() + .ok_or_else(|| { + log::error!("Prefix is not a valid string."); + format!("Prefix is not a valid string.") + }) + .map_err_to_string()? + .split(",") + .collect(); + config = + filter_config_by_prefix(&config, &prefix_list).map_err_to_string()?; + } + + query_map.remove("prefix"); + + if !query_map.is_empty() { + config = filter_config_by_dimensions(&config, &query_map) + .map_err_to_string()?; + } + } + Ok(config) } pub fn get_last_modified(&self) -> Result<DateTime<Utc>, String> { @@ -177,40 +207,31 @@ impl Client { pub fn get_resolved_config( &self, query_data: Map<String, Value>, - keys: Vec<String>, + filter_keys: Option<Vec<String>>, merge_strategy: MergeStrategy, ) -> Result<Map<String, Value>, String> { - let cac = self.eval(query_data, merge_strategy)?; - if keys.is_empty() { - return Ok(cac); + let mut cac = self.eval(query_data, merge_strategy)?; + if let Some(keys) = filter_keys { + cac = filter_keys_by_prefix(cac, &keys.iter().map(|s| s.as_str()).collect()) + .map_err_to_string()?; } - Ok(Map::from_iter( - cac.iter() - .filter_map(|(config, v)| { - if keys.contains(config) { - Some((config.to_owned(), v.to_owned())) - } else { - None - } - }) - .collect::<Vec<(String, Value)>>(), - )) + return Ok(cac); } pub fn get_default_config( &self, - keys: Vec<String>, + filter_keys: Option<Vec<String>>, ) -> Result<Map<String, Value>, String> { let configs = self.config.read().map_err(|e| e.to_string())?; - let default_configs = configs.default_configs.clone(); - if keys.is_empty() { - return Ok(default_configs); + let mut default_configs = configs.default_configs.clone(); + if let Some(keys) = filter_keys { + default_configs = filter_keys_by_prefix( + default_configs, + &keys.iter().map(|s| s.as_str()).collect(), + ) + .map_err_to_string()?; } - let default_configs = default_configs - .into_iter() - .filter(|(item, _)| keys.contains(item)) - .collect::<Map<String, Value>>(); - Ok(default_configs) + return Ok(default_configs); } } @@ -264,3 +285,104 @@ pub static CLIENT_FACTORY: Lazy<ClientFactory> = pub use eval::eval_cac; pub use eval::eval_cac_with_reasoning; pub use eval::merge; + +pub fn filter_keys_by_prefix( + keys: Map<String, Value>, + prefix_list: &HashSet<&str>, +) -> actix_web::Result<Map<String, Value>> { + Ok(keys + .into_iter() + .filter(|(key, _)| { + prefix_list + .iter() + .any(|prefix_str| key.starts_with(prefix_str)) + }) + .collect()) +} + +pub fn filter_config_by_prefix( + config: &Config, + prefix_list: &HashSet<&str>, +) -> actix_web::Result<Config> { + let mut filtered_overrides: Map<String, Value> = Map::new(); + + let filtered_default_config: Map<String, Value> = + filter_keys_by_prefix(config.default_configs.clone(), prefix_list)?; + + for (key, overrides) in &config.overrides { + let overrides_map = overrides + .as_object() + .ok_or_else(|| { + log::error!("failed to decode overrides."); + err::InternalServerErr("failed to decode overrides.".to_string()) + })? + .clone(); + + let filtered_overrides_map: Map<String, Value> = overrides_map + .into_iter() + .filter(|(key, _)| filtered_default_config.contains_key(key)) + .collect(); + + if !filtered_overrides_map.is_empty() { + filtered_overrides.insert(key.clone(), Value::Object(filtered_overrides_map)); + } + } + + let filtered_context: Vec<Context> = config + .contexts + .clone() + .into_iter() + .filter(|context| filtered_overrides.contains_key(&context.override_with_keys[0])) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: filtered_default_config, + }; + + Ok(filtered_config) +} + +pub fn filter_config_by_dimensions( + config: &Config, + query_params_map: &Map<String, Value>, +) -> actix_web::Result<Config> { + let filter_context = |contexts: &Vec<Context>, + query_params_map: &Map<String, Value>| + -> app::Result<Vec<Context>> { + let mut filtered_context: Vec<Context> = Vec::new(); + for context in contexts.iter() { + let dimension = extract_dimensions(&context.condition)?; + let should_add_ctx = dimension.iter().all(|(key, value)| { + query_params_map.get(key).map_or(true, |val| { + val == value || val.as_array().unwrap_or(&vec![]).contains(value) + }) + }); + if should_add_ctx { + filtered_context.push(context.clone()); + } + } + return Ok(filtered_context); + }; + + let filtered_context = filter_context(&config.contexts, &query_params_map)?; + let filtered_overrides: Map<String, Value> = filtered_context + .iter() + .flat_map(|ele| { + let override_with_key = &ele.override_with_keys[0]; + config + .overrides + .get(override_with_key) + .map(|value| (override_with_key.to_string(), value.clone())) + }) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs.clone(), + }; + + Ok(filtered_config) +} diff --git a/crates/context-aware-config/src/api/config/helpers.rs b/crates/context-aware-config/src/api/config/helpers.rs index 9b3f7f35b..4b2fd0d65 100644 --- a/crates/context-aware-config/src/api/config/helpers.rs +++ b/crates/context-aware-config/src/api/config/helpers.rs @@ -25,9 +25,11 @@ fn should_add_ctx( query_params_map: &Map<String, Value>, ) -> superposition::Result<bool> { let dimension = extract_dimensions(&context.condition)?; - Ok(dimension - .iter() - .all(|(key, value)| query_params_map.get(key).map_or(true, |val| val == value))) + Ok(dimension.iter().all(|(key, value)| { + query_params_map.get(key).map_or(true, |val| { + val == value || val.as_array().unwrap_or(&vec![]).contains(value) + }) + })) } pub fn filter_config_by_prefix( diff --git a/docs/client-context-aware-configuration.md b/docs/client-context-aware-configuration.md index eae44efe4..6a9bd9f20 100644 --- a/docs/client-context-aware-configuration.md +++ b/docs/client-context-aware-configuration.md @@ -148,7 +148,7 @@ pub struct Config { ##### Funtion Definition ``` -pub fn get_full_config_state() -> Result<Config, String> +pub fn get_full_config_state_with_filter(query_data: Option<Map<String, Value>>) -> Result<Config, String> ``` #### Get the last modified Time @@ -163,33 +163,33 @@ pub fn get_last_modified() -> Result<DateTime<Utc>, String> #### Evaluate Context to derive configs -Given a context, get overrides for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. +Given a context, get overrides for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. ##### Function Definition ``` -pub fn get_resolved_config(context: Map<String, Value>, keys: Vec<String>) -> Result<Map<String, Value>, String> +pub fn get_resolved_config(context: Map<String, Value>, filter_keys: Option<Vec<String>>) -> Result<Map<String, Value>, String> ``` ##### Params -| Param | type | description | Example value | -| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | -| `context` | Map<String, Value> | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | -| `keys` | Vec<String> | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | Map<String, Value> | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `filter_keys` | Option<Vec<String>> | The keys for which you want the values. If empty, all configuration keys are returned | `Some([payment, network, color])` | #### Get Default Config -The default config for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. +The default config for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. ##### Function Definition ``` -pub fn get_default_config(keys: Vec<String>) -> Result<Map<String, Value>, String> +pub fn get_default_config(filter_keys: Option<Vec<String>>) -> Result<Map<String, Value>, String> ``` ##### Param -| Param | type | description | Example value | -| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | -| `keys` | Vec<String> | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `filter_keys` | Option<Vec<String>> | The keys for which you want the values. If None, all configuration keys are returned | `Some([payment, network, color])` | --- @@ -295,7 +295,7 @@ Get the full config definition of your tenants configuration from superposition. ##### Funtion Definition ``` -getFullConfigState :: ForeignPtr CacClient -> IO (Either Error Value) +getFullConfigStateWithFilter :: ForeignPtr CacClient -> Maybe String -> IO (Either Error Value) ``` #### Get the last modified Time @@ -310,32 +310,32 @@ getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) #### Evaluate Context to derive configs -Given a context, get overrides for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. +Given a context, get overrides for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. ##### Function Definition ``` -getResolvedConfig :: ForeignPtr CacClient -> String -> [String] -> IO (Either Error Value) +getResolvedConfig :: ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error Value) ``` ##### Params -| Param | type | description | Example value | -| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | -| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | -| `keys` | [String] | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `filter_keys` | Maybe([String]) | The keys for which you want the values. If empty, all configuration keys are returned | `Just ([payment, network, color])` | #### Get Default Config -The default config for a specific set of keys, if provided. If empty vector is provided for `keys`, all configs are returned. +The default config for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. ##### Function Definition ``` -getDefaultConfig :: ForeignPtr CacClient -> [String] -> IO (Either Error Value) +getDefaultConfig :: ForeignPtr CacClient -> Maybe [String] -> IO (Either Error Value) ``` -| Param | type | description | Example value | -| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | -| `keys` | [String] | The keys for which you want the values. If empty, all configuration keys are returned | `[payment, network, color]` | +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `filter_keys` | Maybe([String]) | The keys for which you want the values. If Nothing, all configuration keys are returned | `Just ([payment, network, color])`| #### Sample Integration @@ -344,7 +344,7 @@ getDefaultConfig :: ForeignPtr CacClient -> [String] -> IO (Either Error Value) module Main (main) where import Client (getResolvedConfig, createCacClient, getCacClient, - getFullConfigState, getCacLastModified, cacStartPolling, getDefaultConfig) + getFullConfigStateWithFilter, getCacLastModified, cacStartPolling, getDefaultConfig) import Control.Concurrent import Prelude @@ -358,14 +358,16 @@ main = do getCacClient "dev" >>= \case Left err -> putStrLn err Right client -> do - config <- getFullConfigState client - lastModified <- getCacLastModified client - overrides <- getResolvedConfig client "{\"country\": \"India\"}" ["country_image_url", "hyperpay_version"] - defaults <- getDefaultConfig client ["country_image_url", "hyperpay_version"] + config <- getFullConfigStateWithFilter client Nothing + lastModified <- getCacLastModified client + overrides <- getResolvedConfig client "{\"country\": \"India\"}" $ Just ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client $ Just ["country_image_url", "hyperpay_version"] + filteredConfig <- getFullConfigStateWithFilter client $ Just "{\"prefix\": \"hyperpay\"}" print config print lastModified print overrides print defaults + print filteredConfig threadDelay 1000000000 pure () diff --git a/headers/libcac_client.h b/headers/libcac_client.h index 0b9eab167..6a762089f 100644 --- a/headers/libcac_client.h +++ b/headers/libcac_client.h @@ -21,11 +21,11 @@ struct Arc_Client *get_client(const char *tenant); const char *get_last_modified(struct Arc_Client *client); -const char *get_config(struct Arc_Client *client); +const char *get_config(struct Arc_Client *client, const char *query); const char *get_resolved_config(struct Arc_Client *client, const char *query, - const char *keys, + const char *filter_keys, const char *merge_strategy); -const char *get_default_config(struct Arc_Client *client, const char *keys); +const char *get_default_config(struct Arc_Client *client, const char *filter_keys); From 5db1ba42eeb46261f16ce7a9341c5a3c3d956d06 Mon Sep 17 00:00:00 2001 From: "ankit.mahato" <ankit.mahato@juspay.in> Date: Mon, 15 Apr 2024 19:44:25 +0530 Subject: [PATCH 345/352] fix: fixed error in client --- Cargo.lock | 1 + crates/cac_client/Cargo.toml | 1 + crates/cac_client/src/lib.rs | 14 ++++++-------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9838cc728..d0008edee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,6 +613,7 @@ name = "cac_client" version = "0.8.0" dependencies = [ "actix-web", + "anyhow", "cbindgen", "chrono", "derive_more", diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 0c8781616..72ee7b9fa 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -20,6 +20,7 @@ strum_macros = { workspace = true } strum = { workspace = true } tokio = {version = "1.29.1", features = ["full"]} service-utils = { path = "../service-utils" } +anyhow = { workspace = true} [lib] name = "cac_client" diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 75bbabc7a..e75c2e948 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -17,9 +17,7 @@ use std::{ use strum_macros; use utils::core::MapError; -use service_utils::{ - errors::types::Error as err, helpers::extract_dimensions, types as app, -}; +use service_utils::{helpers::extract_dimensions, result, unexpected_error}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { @@ -289,7 +287,7 @@ pub use eval::merge; pub fn filter_keys_by_prefix( keys: Map<String, Value>, prefix_list: &HashSet<&str>, -) -> actix_web::Result<Map<String, Value>> { +) -> result::Result<Map<String, Value>> { Ok(keys .into_iter() .filter(|(key, _)| { @@ -303,7 +301,7 @@ pub fn filter_keys_by_prefix( pub fn filter_config_by_prefix( config: &Config, prefix_list: &HashSet<&str>, -) -> actix_web::Result<Config> { +) -> result::Result<Config> { let mut filtered_overrides: Map<String, Value> = Map::new(); let filtered_default_config: Map<String, Value> = @@ -314,7 +312,7 @@ pub fn filter_config_by_prefix( .as_object() .ok_or_else(|| { log::error!("failed to decode overrides."); - err::InternalServerErr("failed to decode overrides.".to_string()) + unexpected_error!("failed to decode overrides.") })? .clone(); @@ -347,10 +345,10 @@ pub fn filter_config_by_prefix( pub fn filter_config_by_dimensions( config: &Config, query_params_map: &Map<String, Value>, -) -> actix_web::Result<Config> { +) -> result::Result<Config> { let filter_context = |contexts: &Vec<Context>, query_params_map: &Map<String, Value>| - -> app::Result<Vec<Context>> { + -> result::Result<Vec<Context>> { let mut filtered_context: Vec<Context> = Vec::new(); for context in contexts.iter() { let dimension = extract_dimensions(&context.condition)?; From 3405b5d2784c5250552ba29ee254c33aad035f64 Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Tue, 16 Apr 2024 06:37:21 +0000 Subject: [PATCH 346/352] chore(version): v0.39.0 [skip ci] --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 4 ++-- crates/cac_client/CHANGELOG.md | 8 ++++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045ef4e64..2020a2b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.39.0 - 2024-04-16 +### Package updates +- cac_client bumped to cac_client-v0.9.0 +- context-aware-config bumped to context-aware-config-v0.26.0 +### Global changes +#### Bug Fixes +- PICAF-26366 fixed error in client - (d1b1f03) - ankit.mahato +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + ## v0.38.2 - 2024-04-12 ### Package updates - frontend bumped to frontend-v0.5.1 diff --git a/Cargo.lock b/Cargo.lock index d0008edee..30f779e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.8.0" +version = "0.9.0" dependencies = [ "actix-web", "anyhow", @@ -902,7 +902,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.25.2" +version = "0.26.0" dependencies = [ "actix", "actix-cors", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index ceaffeb61..44bb1bd42 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.9.0 - 2024-04-16 +#### Bug Fixes +- PICAF-26366 fixed error in client - (d1b1f03) - ankit.mahato +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + ## cac_client-v0.8.0 - 2024-04-05 #### Bug Fixes - [PICAF-26101] empty key filters should return all keys - (f9dd889) - Kartik diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 72ee7b9fa..5dfb668e2 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.8.0" +version = "0.9.0" edition = "2021" build = "build.rs" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index 315e86694..a3ae180b3 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.26.0 - 2024-04-16 +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + ## context-aware-config-v0.25.2 - 2024-04-10 #### Bug Fixes - PICAF-26366 added service-prefix to functions endpoints - (5492072) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 507e189eb..696102dd2 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.25.2" +version = "0.26.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From bdfdaed1df3e088c8e37da6e4e0cce73793690ae Mon Sep 17 00:00:00 2001 From: Saurav Suman <saurav.suman@juspay.in> Date: Mon, 15 Apr 2024 15:13:31 +0530 Subject: [PATCH 347/352] fix: removed audit log middleware and reduced max db connection pool size to 2 --- crates/cac_client/src/lib.rs | 6 +- .../src/api/config/handlers.rs | 34 +++++- crates/context-aware-config/src/main.rs | 11 +- .../src/middlewares/audit_response_header.rs | 101 ------------------ .../src/middlewares/mod.rs | 1 - 5 files changed, 37 insertions(+), 116 deletions(-) delete mode 100644 crates/context-aware-config/src/middlewares/audit_response_header.rs diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index e75c2e948..11b09fa7f 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -28,9 +28,9 @@ pub struct Context { #[repr(C)] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { - contexts: Vec<Context>, - overrides: Map<String, Value>, - default_configs: Map<String, Value>, + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, } #[derive(strum_macros::EnumString)] diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context-aware-config/src/api/config/handlers.rs index 1d2c0936c..a25f0f26f 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context-aware-config/src/api/config/handlers.rs @@ -23,6 +23,7 @@ use service_utils::service::types::DbConnection; use service_utils::{bad_argument, db_error, unexpected_error}; use service_utils::result as superposition; +use uuid::Uuid; pub fn endpoints() -> Scope { Scope::new("") @@ -31,6 +32,29 @@ pub fn endpoints() -> Scope { .service(get_filtered_config) } +pub fn add_audit_header( + conn: &mut PooledConnection<ConnectionManager<PgConnection>>, + mut res: HttpResponse, +) -> superposition::Result<HttpResponse> { + let header_name = HeaderName::from_static("x-audit-id"); + if let Ok(uuid) = event_log::event_log + .select(event_log::id) + .filter(event_log::table_name.eq("contexts")) + .order_by(event_log::timestamp.desc()) + .first::<Uuid>(conn) + { + let uuid_string = uuid.to_string(); + if let Ok(header_value) = HeaderValue::from_str(&uuid_string) { + res.headers_mut().insert(header_name, header_value); + } else { + log::error!("Failed to convert UUID to string"); + } + } else { + log::error!("Failed to fetch contexts from event_log"); + } + Ok(res) +} + fn add_last_modified_header( max_created_at: Option<NaiveDateTime>, mut res: HttpResponse, @@ -177,7 +201,10 @@ async fn get( config = filter_config_by_dimensions(&config, &query_params_map)? } - add_last_modified_header(max_created_at, HttpResponse::Ok().json(config)) + let resp = HttpResponse::Ok().json(config); + let audit_resp = add_audit_header(&mut conn, resp)?; + + add_last_modified_header(max_created_at, audit_resp) } #[get("/resolve")] @@ -261,7 +288,8 @@ async fn get_resolved_config( })?, ) }; - add_last_modified_header(max_created_at, response) + let audit_resp = add_audit_header(&mut conn, response)?; + add_last_modified_header(max_created_at, audit_resp) } #[get("/filter")] @@ -311,5 +339,5 @@ async fn get_filtered_config( default_configs: config.default_configs, }; - Ok(HttpResponse::Ok().json(filtered_config)) + add_audit_header(&mut conn, HttpResponse::Ok().json(filtered_config)) } diff --git a/crates/context-aware-config/src/main.rs b/crates/context-aware-config/src/main.rs index eb1567333..be88892fd 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/context-aware-config/src/main.rs @@ -6,10 +6,7 @@ mod logger; mod middlewares; mod validation_functions; -use crate::middlewares::{ - audit_response_header::{AuditHeader, TableName}, - cookie_to_header::CookieToHeader, -}; +use crate::middlewares::cookie_to_header::CookieToHeader; use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; use api::*; use auth::fill_service_prefix; @@ -91,7 +88,7 @@ async fn main() -> Result<()> { let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); - let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 3); + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); let api_host: String = get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); @@ -234,8 +231,7 @@ async fn main() -> Result<()> { .service(default_config::endpoints()), ) .service( - scope("/config") - .wrap(AuditHeader::new(TableName::Contexts)) + scope("/config") .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(config::endpoints()), ) @@ -284,7 +280,6 @@ async fn main() -> Result<()> { ) .service( scope("/config") - .wrap(AuditHeader::new(TableName::Contexts)) .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(config::endpoints()), ) diff --git a/crates/context-aware-config/src/middlewares/audit_response_header.rs b/crates/context-aware-config/src/middlewares/audit_response_header.rs deleted file mode 100644 index 0003c2694..000000000 --- a/crates/context-aware-config/src/middlewares/audit_response_header.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::future::{ready, Ready}; - -use actix_web::{ - dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, - http::header::{HeaderName, HeaderValue}, - Error, -}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use futures_util::future::LocalBoxFuture; -use service_utils::service::types::DbConnection; - -use crate::db::schema::event_log::dsl as event_log; -use std::rc::Rc; -use uuid::Uuid; - -#[derive(Clone, Copy, Debug, strum_macros::Display)] -#[strum(serialize_all = "snake_case")] -#[allow(dead_code)] -pub enum TableName { - Contexts, - DefaultConfigs, - Dimensions, - Experiments, -} - -pub struct AuditHeader { - table_name: TableName, -} - -impl AuditHeader { - pub fn new(table_name: TableName) -> Self { - AuditHeader { table_name } - } -} - -impl<S, B> Transform<S, ServiceRequest> for AuditHeader -where - S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse<B>; - type Error = Error; - type InitError = (); - type Transform = AuditHeaderMiddleware<S>; - type Future = Ready<Result<Self::Transform, Self::InitError>>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(AuditHeaderMiddleware { - service: Rc::new(service), - table_name: self.table_name, - })) - } -} - -pub struct AuditHeaderMiddleware<S> { - service: Rc<S>, - table_name: TableName, -} - -impl<S, B> Service<ServiceRequest> for AuditHeaderMiddleware<S> -where - S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse<B>; - type Error = Error; - type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; - - forward_ready!(service); - - fn call(&self, mut req: ServiceRequest) -> Self::Future { - let srv = self.service.clone(); - let table_name = self.table_name; - - Box::pin(async move { - let db_conn = req.extract::<DbConnection>().await?; - let DbConnection(mut conn) = db_conn; - - let mut res = srv.call(req).await?; - - let uuid = event_log::event_log - .select(event_log::id) - .filter(event_log::table_name.eq(table_name.to_string())) - .order_by(event_log::timestamp.desc()) - .first::<Uuid>(&mut conn); - - if let Ok(uuid) = uuid { - res.headers_mut().insert( - HeaderName::from_static("x-audit-id"), - HeaderValue::from_str(&uuid.to_string()) - .unwrap_or_else(|_| HeaderValue::from_static("invalid")), - ); - } else { - log::error!("Unable to fetch uuid"); - } - Ok(res) - }) - } -} diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/context-aware-config/src/middlewares/mod.rs index 4533bf242..e1b697d54 100644 --- a/crates/context-aware-config/src/middlewares/mod.rs +++ b/crates/context-aware-config/src/middlewares/mod.rs @@ -1,4 +1,3 @@ -pub mod audit_response_header; pub mod cookie_to_header; use actix_web::{dev::RequestHead, http::header::HeaderValue}; From 9cf8f81c50426218d1bfaf70b147430790d6fc4b Mon Sep 17 00:00:00 2001 From: Jenkins <bitbucket.jenkins.read@juspay.in> Date: Wed, 17 Apr 2024 07:29:57 +0000 Subject: [PATCH 348/352] chore(version): v0.39.1 [skip ci] --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 4 ++-- crates/cac_client/CHANGELOG.md | 6 ++++++ crates/cac_client/Cargo.toml | 2 +- crates/context-aware-config/CHANGELOG.md | 6 ++++++ crates/context-aware-config/Cargo.toml | 2 +- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2020a2b11..e9dceb6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.39.1 - 2024-04-17 +### Package updates +- cac_client bumped to cac_client-v0.9.1 +- context-aware-config bumped to context-aware-config-v0.26.1 +### Global changes + +- - - + ## v0.39.0 - 2024-04-16 ### Package updates - cac_client bumped to cac_client-v0.9.0 diff --git a/Cargo.lock b/Cargo.lock index 30f779e7d..145c51bce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "cac_client" -version = "0.9.0" +version = "0.9.1" dependencies = [ "actix-web", "anyhow", @@ -902,7 +902,7 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "context-aware-config" -version = "0.26.0" +version = "0.26.1" dependencies = [ "actix", "actix-cors", diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md index 44bb1bd42..0d3d80e74 100644 --- a/crates/cac_client/CHANGELOG.md +++ b/crates/cac_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## cac_client-v0.9.1 - 2024-04-17 +#### Bug Fixes +- [PICAF-26653] removed audit log middleware and reduced max db connection pool size to 2 - (82022eb) - Saurav Suman + +- - - + ## cac_client-v0.9.0 - 2024-04-16 #### Bug Fixes - PICAF-26366 fixed error in client - (d1b1f03) - ankit.mahato diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 5dfb668e2..1337ac8ad 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cac_client" -version = "0.9.0" +version = "0.9.1" edition = "2021" build = "build.rs" diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context-aware-config/CHANGELOG.md index a3ae180b3..798075d69 100644 --- a/crates/context-aware-config/CHANGELOG.md +++ b/crates/context-aware-config/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## context-aware-config-v0.26.1 - 2024-04-17 +#### Bug Fixes +- [PICAF-26653] removed audit log middleware and reduced max db connection pool size to 2 - (82022eb) - Saurav Suman + +- - - + ## context-aware-config-v0.26.0 - 2024-04-16 #### Features - PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato diff --git a/crates/context-aware-config/Cargo.toml b/crates/context-aware-config/Cargo.toml index 696102dd2..1fa30bad2 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context-aware-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-aware-config" -version = "0.26.0" +version = "0.26.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From f1dca84371d187ed24e24381ac0b318257fc97c0 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Wed, 17 Apr 2024 11:23:48 +0530 Subject: [PATCH 349/352] feat: created new types crate for superposition movement --- crates/superposition_types/Cargo.toml | 18 +++++++ crates/superposition_types/src/lib.rs | 67 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 crates/superposition_types/Cargo.toml create mode 100644 crates/superposition_types/src/lib.rs diff --git a/crates/superposition_types/Cargo.toml b/crates/superposition_types/Cargo.toml new file mode 100644 index 000000000..1bb353762 --- /dev/null +++ b/crates/superposition_types/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "superposition_types" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +actix = { workspace = true } +actix-web = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +derive_more = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs new file mode 100644 index 000000000..fe8051378 --- /dev/null +++ b/crates/superposition_types/src/lib.rs @@ -0,0 +1,67 @@ +use actix::fut::{ready, Ready}; +use actix_web::{dev::Payload, error, FromRequest, HttpMessage, HttpRequest}; +use log::error; +use serde_json::json; + +pub trait SuperpositionUser { + fn get_email(&self) -> String; + fn get_username(&self) -> String; + fn get_auth_token(&self) -> String; +} + +#[derive(Debug, Clone)] +pub struct User { + email: String, + username: String, + auth_token: String, +} + +impl SuperpositionUser for User { + fn get_email(&self) -> String { + self.email.clone() + } + + fn get_username(&self) -> String { + self.username.clone() + } + + fn get_auth_token(&self) -> String { + self.auth_token.clone() + } +} + +impl Default for User { + fn default() -> Self { + Self { + email: "superposition@juspay.in".into(), + username: "superposition".into(), + auth_token: "1234abcd".into(), + } + } +} + +impl From<Box<dyn SuperpositionUser>> for User { + fn from(value: Box<dyn SuperpositionUser>) -> Self { + User { + email: value.get_email(), + username: value.get_username(), + auth_token: value.get_auth_token(), + } + } +} + +impl FromRequest for User { + type Error = actix_web::error::Error; + type Future = Ready<Result<Self, Self::Error>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(user) = req.extensions().get::<User>() { + ready(Ok(user.to_owned())) + } else { + error!("No user was found while validating token"); + ready(Err(error::ErrorUnauthorized( + json!({"message":"invalid token provided"}), + ))) + } + } +} From 4ae27f2727211eba49ffafb4464db73a686c99d1 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Wed, 17 Apr 2024 16:22:10 +0530 Subject: [PATCH 350/352] feat: add auth_type so this can be used when making API calls --- cog.toml | 3 ++- crates/superposition_types/src/lib.rs | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cog.toml b/cog.toml index ae80e8d15..2b84d2a47 100644 --- a/cog.toml +++ b/cog.toml @@ -32,4 +32,5 @@ external = { path = "crates/external" } frontend = { path = "crates/frontend" } caclang = { path = "crates/caclang" } cac_client = { path = "crates/cac_client" } -experimentation_client = { path = "crates/experimentation_client" } \ No newline at end of file +experimentation_client = { path = "crates/experimentation_client" } +superposition_types = { path = "crates/superposition_types" } \ No newline at end of file diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs index fe8051378..aab543edc 100644 --- a/crates/superposition_types/src/lib.rs +++ b/crates/superposition_types/src/lib.rs @@ -7,13 +7,15 @@ pub trait SuperpositionUser { fn get_email(&self) -> String; fn get_username(&self) -> String; fn get_auth_token(&self) -> String; + fn get_auth_type(&self) -> String; } #[derive(Debug, Clone)] pub struct User { - email: String, - username: String, - auth_token: String, + pub email: String, + pub username: String, + pub auth_token: String, + pub auth_type: String, } impl SuperpositionUser for User { @@ -28,6 +30,10 @@ impl SuperpositionUser for User { fn get_auth_token(&self) -> String { self.auth_token.clone() } + + fn get_auth_type(&self) -> String { + self.auth_type.clone() + } } impl Default for User { @@ -36,6 +42,7 @@ impl Default for User { email: "superposition@juspay.in".into(), username: "superposition".into(), auth_token: "1234abcd".into(), + auth_type: "Bearer".into(), } } } @@ -46,6 +53,7 @@ impl From<Box<dyn SuperpositionUser>> for User { email: value.get_email(), username: value.get_username(), auth_token: value.get_auth_token(), + auth_type: value.get_auth_type(), } } } From 5f7af156fe76905b88cfb765c7c73546418e5be9 Mon Sep 17 00:00:00 2001 From: Kartik <kartik.gajendra@juspay.in> Date: Wed, 17 Apr 2024 16:22:55 +0530 Subject: [PATCH 351/352] feat: ready for open source! - migrated crate names to meet rust standards - decoupled authentication - created OS crate called superposition --- .env.example | 4 +- Cargo.lock | 233 ++++++++++++++--- Cargo.toml | 22 +- Dockerfile | 8 +- cog.toml | 6 +- crates/cac_client/Cargo.toml | 5 +- crates/cac_client/src/lib.rs | 12 +- .../CHANGELOG.md | 0 .../Cargo.toml | 10 +- .../diesel.toml | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../2024-02-19-125126_functions/down.sql | 0 .../2024-02-19-125126_functions/up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../package-lock.json | 0 .../package.json | 0 .../samples/default_config.json | 0 .../samples/dimensions.json | 0 .../samples/overrides.json | 0 .../src/api/audit_log/handlers.rs | 0 .../src/api/audit_log/mod.rs | 0 .../src/api/audit_log/types.rs | 0 .../src/api/config/handlers.rs | 4 +- .../src/api/config/helpers.rs | 0 .../src/api/config/mod.rs | 0 .../src/api/config/types.rs | 0 .../src/api/context/handlers.rs | 31 ++- .../src/api/context/helpers.rs | 0 .../src/api/context/mod.rs | 0 .../src/api/context/types.rs | 0 .../src/api/default_config/handlers.rs | 7 +- .../src/api/default_config/mod.rs | 0 .../src/api/default_config/types.rs | 0 .../src/api/dimension/handlers.rs | 6 +- .../src/api/dimension/mod.rs | 0 .../src/api/dimension/types.rs | 0 .../src/api/dimension/utils.rs | 0 .../src/api/functions/handlers.rs | 20 +- .../src/api/functions/helpers.rs | 0 .../src/api/functions/mod.rs | 0 .../src/api/functions/types.rs | 0 .../src/api/mod.rs | 0 .../src/db/mod.rs | 0 .../src/db/models.rs | 0 .../src/db/schema.rs | 0 .../src/helpers.rs | 0 .../src/lib.rs | 0 .../src/middlewares/cookie_to_header.rs | 0 .../src/middlewares/mod.rs | 1 + .../src/validation_functions.rs | 0 .../tests/cac_tests.rs | 0 .../tests/index.js | 0 .../CHANGELOG.md | 0 .../Cargo.toml | 6 +- .../diesel.toml | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../down.sql | 0 .../up.sql | 0 .../src/api/experiments/handlers.rs | 36 +-- .../src/api/experiments/helpers.rs | 0 .../src/api/experiments/mod.rs | 0 .../src/api/experiments/types.rs | 0 .../src/api/mod.rs | 0 .../src/db/mod.rs | 0 .../src/db/models.rs | 0 .../src/db/schema.rs | 0 .../src/lib.rs | 0 .../src/schema.patch | 0 .../tests/experimentation_tests.rs | 0 crates/external/Cargo.toml | 9 +- .../external/src/api/external_api/handlers.rs | 15 +- .../external/src/api/external_api/helpers.rs | 1 - .../src/components/context_form/utils.rs | 6 +- .../components/function_form/function_form.rs | 2 +- .../src/pages/function/function_list.rs | 23 +- crates/juspay_superposition/Cargo.toml | 64 +++++ .../src/auth.rs | 0 .../src/logger.rs | 0 .../src/main.rs | 31 ++- .../src/middlewares/mod.rs | 2 - .../CHANGELOG.md | 36 +-- .../Cargo.toml | 5 +- .../src/aws/kms.rs | 0 .../src/aws/mod.rs | 0 .../src/db/mod.rs | 0 .../src/db/pgschema_manager.rs | 0 .../src/db/utils.rs | 0 .../src/helpers.rs | 0 .../src/lib.rs | 0 .../src/macros.rs | 0 .../src/middlewares/app_scope.rs | 1 - .../src/middlewares/mod.rs | 0 .../src/middlewares/tenant.rs | 0 .../src/result.rs | 0 .../src/service/mod.rs | 0 .../src/service/types.rs | 1 - crates/superposition/Cargo.toml | 61 +++++ crates/superposition/src/main.rs | 246 ++++++++++++++++++ makefile | 41 ++- scripts/create-tenant.sh | 4 +- scripts/legacy-db-setup.sh | 20 +- 110 files changed, 764 insertions(+), 215 deletions(-) rename crates/{context-aware-config => context_aware_config}/CHANGELOG.md (100%) rename crates/{context-aware-config => context_aware_config}/Cargo.toml (87%) rename crates/{context-aware-config => context_aware_config}/diesel.toml (100%) rename crates/{context-aware-config => context_aware_config}/migrations/00000000000000_diesel_initial_setup/down.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/00000000000000_diesel_initial_setup/up.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2023-10-16-133815_context-aware-config-init/down.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2023-10-16-133815_context-aware-config-init/up.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-01-23-123559_audit_log_partitions/down.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-01-23-123559_audit_log_partitions/up.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-02-19-125126_functions/down.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-02-19-125126_functions/up.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql (100%) rename crates/{context-aware-config => context_aware_config}/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql (100%) rename crates/{context-aware-config => context_aware_config}/package-lock.json (100%) rename crates/{context-aware-config => context_aware_config}/package.json (100%) rename crates/{context-aware-config => context_aware_config}/samples/default_config.json (100%) rename crates/{context-aware-config => context_aware_config}/samples/dimensions.json (100%) rename crates/{context-aware-config => context_aware_config}/samples/overrides.json (100%) rename crates/{context-aware-config => context_aware_config}/src/api/audit_log/handlers.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/audit_log/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/audit_log/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/config/handlers.rs (98%) rename crates/{context-aware-config => context_aware_config}/src/api/config/helpers.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/config/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/config/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/context/handlers.rs (96%) rename crates/{context-aware-config => context_aware_config}/src/api/context/helpers.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/context/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/context/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/default_config/handlers.rs (98%) rename crates/{context-aware-config => context_aware_config}/src/api/default_config/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/default_config/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/dimension/handlers.rs (95%) rename crates/{context-aware-config => context_aware_config}/src/api/dimension/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/dimension/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/dimension/utils.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/functions/handlers.rs (97%) rename crates/{context-aware-config => context_aware_config}/src/api/functions/helpers.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/functions/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/functions/types.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/api/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/db/mod.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/db/models.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/db/schema.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/helpers.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/lib.rs (100%) rename crates/{context-aware-config => context_aware_config}/src/middlewares/cookie_to_header.rs (100%) create mode 100644 crates/context_aware_config/src/middlewares/mod.rs rename crates/{context-aware-config => context_aware_config}/src/validation_functions.rs (100%) rename crates/{context-aware-config => context_aware_config}/tests/cac_tests.rs (100%) rename crates/{context-aware-config => context_aware_config}/tests/index.js (100%) rename crates/{experimentation-platform => experimentation_platform}/CHANGELOG.md (100%) rename crates/{experimentation-platform => experimentation_platform}/Cargo.toml (81%) rename crates/{experimentation-platform => experimentation_platform}/diesel.toml (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/00000000000000_diesel_initial_setup/down.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/00000000000000_diesel_initial_setup/up.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/2023-10-16-134612_experimentation-init/down.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/2023-10-16-134612_experimentation-init/up.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/2024-01-18-063937_audit_log_partitions/down.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/migrations/2024-01-18-063937_audit_log_partitions/up.sql (100%) rename crates/{experimentation-platform => experimentation_platform}/src/api/experiments/handlers.rs (97%) rename crates/{experimentation-platform => experimentation_platform}/src/api/experiments/helpers.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/api/experiments/mod.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/api/experiments/types.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/api/mod.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/db/mod.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/db/models.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/db/schema.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/lib.rs (100%) rename crates/{experimentation-platform => experimentation_platform}/src/schema.patch (100%) rename crates/{experimentation-platform => experimentation_platform}/tests/experimentation_tests.rs (100%) create mode 100644 crates/juspay_superposition/Cargo.toml rename crates/{context-aware-config => juspay_superposition}/src/auth.rs (100%) rename crates/{context-aware-config => juspay_superposition}/src/logger.rs (100%) rename crates/{context-aware-config => juspay_superposition}/src/main.rs (95%) rename crates/{context-aware-config => juspay_superposition}/src/middlewares/mod.rs (97%) rename crates/{service-utils => service_utils}/CHANGELOG.md (83%) rename crates/{service-utils => service_utils}/Cargo.toml (90%) rename crates/{service-utils => service_utils}/src/aws/kms.rs (100%) rename crates/{service-utils => service_utils}/src/aws/mod.rs (100%) rename crates/{service-utils => service_utils}/src/db/mod.rs (100%) rename crates/{service-utils => service_utils}/src/db/pgschema_manager.rs (100%) rename crates/{service-utils => service_utils}/src/db/utils.rs (100%) rename crates/{service-utils => service_utils}/src/helpers.rs (100%) rename crates/{service-utils => service_utils}/src/lib.rs (100%) rename crates/{service-utils => service_utils}/src/macros.rs (100%) rename crates/{service-utils => service_utils}/src/middlewares/app_scope.rs (99%) rename crates/{service-utils => service_utils}/src/middlewares/mod.rs (100%) rename crates/{service-utils => service_utils}/src/middlewares/tenant.rs (100%) rename crates/{service-utils => service_utils}/src/result.rs (100%) rename crates/{service-utils => service_utils}/src/service/mod.rs (100%) rename crates/{service-utils => service_utils}/src/service/types.rs (99%) create mode 100644 crates/superposition/Cargo.toml create mode 100644 crates/superposition/src/main.rs diff --git a/.env.example b/.env.example index 7820179ae..907b62a2b 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,6 @@ ENABLE_TENANT_AND_SCOPE=true TENANT_VALIDATION_ENABLED=false TENANTS=dev,test TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" -DASHBOARD_AUTH_URL="https://dashboard.sandbox.juspay.in/ec/v1/authorize" -SERVICE_PREFIX="superposition" +DASHBOARD_AUTH_URL="https://sandbox.portal.juspay.in/ec/v1/authorize" +SERVICE_PREFIX="" SERVICE_NAME="CAC" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 145c51bce..219b11a76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,7 +623,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "service-utils", + "service_utils", "strum", "strum_macros", "tokio", @@ -901,7 +901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] -name = "context-aware-config" +name = "context_aware_config" version = "0.26.1" dependencies = [ "actix", @@ -915,13 +915,12 @@ dependencies = [ "bytes", "cac_client", "chrono", - "dashboard-auth", "derive_more", "diesel", "diesel-derive-enum", "dotenv", "env_logger 0.8.4", - "experimentation-platform", + "experimentation_platform", "external", "frontend", "futures", @@ -941,11 +940,11 @@ dependencies = [ "rusoto_signature", "serde", "serde_json", - "service-utils", + "service_utils", "strum", "strum_macros", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", "tracing-log", - "tracing-utils", "urlencoding", "uuid", "valuable", @@ -1156,8 +1155,8 @@ dependencies = [ [[package]] name = "dashboard-auth" -version = "0.4.2" -source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.5.4#3abd65d22652613bad6ac0f739fae2f2e69daef6" +version = "0.6.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.9.0#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" dependencies = [ "actix", "actix-web", @@ -1169,6 +1168,25 @@ dependencies = [ "reqwest", "serde", "serde_json", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0)", +] + +[[package]] +name = "dashboard-auth" +version = "0.6.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "dotenv", + "env_logger 0.10.0", + "futures-util", + "log", + "reqwest", + "serde", + "serde_json", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0)", ] [[package]] @@ -1399,43 +1417,43 @@ dependencies = [ ] [[package]] -name = "experimentation-platform" -version = "0.12.0" +name = "experimentation_client" +version = "0.5.0" dependencies = [ - "actix", - "actix-web", - "anyhow", + "cbindgen", "chrono", - "dashboard-auth", "derive_more", - "diesel", - "diesel-derive-enum", "dotenv", - "env_logger 0.8.4", + "jsonlogic", "log", + "once_cell", "reqwest", - "rs-snowflake", "serde", "serde_json", - "service-utils", - "uuid", + "tokio", ] [[package]] -name = "experimentation_client" -version = "0.5.0" +name = "experimentation_platform" +version = "0.12.0" dependencies = [ - "cbindgen", + "actix", + "actix-web", + "anyhow", "chrono", "derive_more", + "diesel", + "diesel-derive-enum", "dotenv", - "jsonlogic", + "env_logger 0.8.4", "log", - "once_cell", "reqwest", + "rs-snowflake", "serde", "serde_json", - "tokio", + "service_utils", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", + "uuid", ] [[package]] @@ -1446,14 +1464,15 @@ dependencies = [ "actix-web", "anyhow", "chrono", - "dashboard-auth", + "dashboard-auth 0.6.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.9.0)", "dotenv", - "experimentation-platform", + "experimentation_platform", "log", "reqwest", "serde", "serde_json", - "service-utils", + "service_utils", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", ] [[package]] @@ -2120,6 +2139,58 @@ dependencies = [ "uuid", ] +[[package]] +name = "juspay_superposition" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-http", + "actix-web", + "anyhow", + "base64 0.21.2", + "blake3", + "bytes", + "cac_client", + "chrono", + "context_aware_config", + "dashboard-auth 0.6.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git)", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "experimentation_platform", + "external", + "frontend", + "futures", + "futures-util", + "itertools 0.10.5", + "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "log", + "rand", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "tracing-log", + "tracing-utils", + "urlencoding", + "uuid", + "valuable", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3536,7 +3607,7 @@ dependencies = [ ] [[package]] -name = "service-utils" +name = "service_utils" version = "0.13.0" dependencies = [ "actix", @@ -3544,7 +3615,6 @@ dependencies = [ "anyhow", "base64 0.21.2", "bytes", - "dashboard-auth", "derive_more", "diesel", "dotenv", @@ -3716,6 +3786,103 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "superposition" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-http", + "actix-web", + "anyhow", + "base64 0.21.2", + "blake3", + "bytes", + "cac_client", + "chrono", + "context_aware_config", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "experimentation_platform", + "frontend", + "futures", + "futures-util", + "itertools 0.10.5", + "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "log", + "rand", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", + "tracing-log", + "urlencoding", + "uuid", + "valuable", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0#086131051b61b1a62989019794c4893251d204a6" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git#086131051b61b1a62989019794c4893251d204a6" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + [[package]] name = "sval" version = "2.6.1" @@ -4124,8 +4291,8 @@ dependencies = [ [[package]] name = "tracing-utils" -version = "0.1.0" -source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#d89ab1e04e193c7fc01ef2d96e144f8a7474c04f" +version = "0.3.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" dependencies = [ "actix-http", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 411d8c88f..7a20c9bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,21 +2,24 @@ resolver = "2" members = [ - "crates/service-utils", - "crates/context-aware-config", - "crates/experimentation-platform", + "crates/service_utils", + "crates/context_aware_config", + "crates/experimentation_platform", "crates/external", - "crates/service-utils", + "crates/service_utils", "crates/experimentation_client", "crates/cac_client", "crates/experimentation_client_integration_example", "crates/frontend", - "crates/caclang" -] + "crates/caclang", + "crates/juspay_superposition", + "crates/superposition", + "crates/superposition_types" + ] [[workspace.metadata.leptos]] name = "cac" -bin-package = "context-aware-config" +bin-package = "juspay_superposition" output-name = "frontend" lib-package = "frontend" site-root = "target/site" @@ -52,10 +55,7 @@ anyhow = "1.0.75" strum_macros = "0.25" strum = "0.25" blake3 = "1.3.3" -# juspay dependencies -dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.5.4"} -tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.1.0" } leptos = { version = "0.5.2" } leptos_meta = { version = "0.5.2" } leptos_router = { version = "0.5.2" } -thiserror = { version = "1.0.57" } \ No newline at end of file +thiserror = { version = "1.0.57" } diff --git a/Dockerfile b/Dockerfile index c3b0df1f1..825d3540e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,9 @@ COPY . . RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh RUN npm ci --loglevel=info -RUN cd crates/context-aware-config/ && npm ci +RUN cd crates/context_aware_config/ && npm ci RUN mkdir -p target/node_modules -RUN cp -a crates/context-aware-config/node_modules target/ +RUN cp -a crates/context_aware_config/node_modules target/ # building frontend RUN --mount=type=ssh cd crates/frontend \ @@ -53,10 +53,10 @@ ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" RUN node --version -COPY --from=builder /build/target/release/context-aware-config /app/context-aware-config +COPY --from=builder /build/target/release/juspay_superposition /app/juspay_superposition COPY --from=builder /build/Cargo.toml /app/Cargo.toml COPY --from=builder /build/target/site /app/target/site COPY --from=builder /build/target/node_modules /app/target/node_modules ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT -CMD ["/app/context-aware-config"] \ No newline at end of file +CMD ["/app/juspay_superposition"] \ No newline at end of file diff --git a/cog.toml b/cog.toml index 2b84d2a47..758a5cacd 100644 --- a/cog.toml +++ b/cog.toml @@ -25,9 +25,9 @@ authors = [] [bump_profiles] [packages] -context-aware-config = { path = "crates/context-aware-config" } -experimentation-platform = { path = "crates/experimentation-platform" } -service-utils = { path = "crates/service-utils" } +context_aware_config = { path = "crates/context_aware_config" } +experimentation_platform = { path = "crates/experimentation_platform" } +service_utils = { path = "crates/service_utils" } external = { path = "crates/external" } frontend = { path = "crates/frontend" } caclang = { path = "crates/caclang" } diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml index 1337ac8ad..198772578 100644 --- a/crates/cac_client/Cargo.toml +++ b/crates/cac_client/Cargo.toml @@ -19,9 +19,8 @@ log = { workspace = true } strum_macros = { workspace = true } strum = { workspace = true } tokio = {version = "1.29.1", features = ["full"]} -service-utils = { path = "../service-utils" } -anyhow = { workspace = true} - +service_utils = { path = "../service_utils" } +anyhow = { workspace = true } [lib] name = "cac_client" crate-type = ["cdylib", "lib"] diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs index 11b09fa7f..84de26842 100644 --- a/crates/cac_client/src/lib.rs +++ b/crates/cac_client/src/lib.rs @@ -17,7 +17,9 @@ use std::{ use strum_macros; use utils::core::MapError; -use service_utils::{helpers::extract_dimensions, result, unexpected_error}; +use service_utils::{ + helpers::extract_dimensions, result as superposition, unexpected_error, +}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Context { @@ -287,7 +289,7 @@ pub use eval::merge; pub fn filter_keys_by_prefix( keys: Map<String, Value>, prefix_list: &HashSet<&str>, -) -> result::Result<Map<String, Value>> { +) -> superposition::Result<Map<String, Value>> { Ok(keys .into_iter() .filter(|(key, _)| { @@ -301,7 +303,7 @@ pub fn filter_keys_by_prefix( pub fn filter_config_by_prefix( config: &Config, prefix_list: &HashSet<&str>, -) -> result::Result<Config> { +) -> superposition::Result<Config> { let mut filtered_overrides: Map<String, Value> = Map::new(); let filtered_default_config: Map<String, Value> = @@ -345,10 +347,10 @@ pub fn filter_config_by_prefix( pub fn filter_config_by_dimensions( config: &Config, query_params_map: &Map<String, Value>, -) -> result::Result<Config> { +) -> superposition::Result<Config> { let filter_context = |contexts: &Vec<Context>, query_params_map: &Map<String, Value>| - -> result::Result<Vec<Context>> { + -> superposition::Result<Vec<Context>> { let mut filtered_context: Vec<Context> = Vec::new(); for context in contexts.iter() { let dimension = extract_dimensions(&context.condition)?; diff --git a/crates/context-aware-config/CHANGELOG.md b/crates/context_aware_config/CHANGELOG.md similarity index 100% rename from crates/context-aware-config/CHANGELOG.md rename to crates/context_aware_config/CHANGELOG.md diff --git a/crates/context-aware-config/Cargo.toml b/crates/context_aware_config/Cargo.toml similarity index 87% rename from crates/context-aware-config/Cargo.toml rename to crates/context_aware_config/Cargo.toml index 1fa30bad2..20534215b 100644 --- a/crates/context-aware-config/Cargo.toml +++ b/crates/context_aware_config/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "context-aware-config" +name = "context_aware_config" version = "0.26.1" edition = "2021" @@ -44,8 +44,8 @@ urlencoding = { workspace = true } jsonschema = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls"] } rand = { workspace = true } -service-utils = { path = "../service-utils" } -experimentation-platform = { path = "../experimentation-platform" } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } tracing-log = "0.1.3" valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} itertools = "0.10.5" @@ -59,7 +59,5 @@ leptos = { workspace = true } leptos_meta = { workspace = true } leptos_router = { workspace = true } actix-files = { version = "0.6" } - anyhow = { workspace = true } -dashboard-auth = { workspace = true } -tracing-utils = { workspace = true } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } diff --git a/crates/context-aware-config/diesel.toml b/crates/context_aware_config/diesel.toml similarity index 100% rename from crates/context-aware-config/diesel.toml rename to crates/context_aware_config/diesel.toml diff --git a/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/down.sql rename to crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from crates/context-aware-config/migrations/00000000000000_diesel_initial_setup/up.sql rename to crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/up.sql diff --git a/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/down.sql b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/down.sql similarity index 100% rename from crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/down.sql rename to crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/down.sql diff --git a/crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/up.sql similarity index 100% rename from crates/context-aware-config/migrations/2023-10-16-133815_context-aware-config-init/up.sql rename to crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/up.sql diff --git a/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/down.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/down.sql rename to crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/down.sql diff --git a/crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/up.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-01-23-123559_audit_log_partitions/up.sql rename to crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/up.sql diff --git a/crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql b/crates/context_aware_config/migrations/2024-02-19-125126_functions/down.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-02-19-125126_functions/down.sql rename to crates/context_aware_config/migrations/2024-02-19-125126_functions/down.sql diff --git a/crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql b/crates/context_aware_config/migrations/2024-02-19-125126_functions/up.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-02-19-125126_functions/up.sql rename to crates/context_aware_config/migrations/2024-02-19-125126_functions/up.sql diff --git a/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql rename to crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql diff --git a/crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql similarity index 100% rename from crates/context-aware-config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql rename to crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql diff --git a/crates/context-aware-config/package-lock.json b/crates/context_aware_config/package-lock.json similarity index 100% rename from crates/context-aware-config/package-lock.json rename to crates/context_aware_config/package-lock.json diff --git a/crates/context-aware-config/package.json b/crates/context_aware_config/package.json similarity index 100% rename from crates/context-aware-config/package.json rename to crates/context_aware_config/package.json diff --git a/crates/context-aware-config/samples/default_config.json b/crates/context_aware_config/samples/default_config.json similarity index 100% rename from crates/context-aware-config/samples/default_config.json rename to crates/context_aware_config/samples/default_config.json diff --git a/crates/context-aware-config/samples/dimensions.json b/crates/context_aware_config/samples/dimensions.json similarity index 100% rename from crates/context-aware-config/samples/dimensions.json rename to crates/context_aware_config/samples/dimensions.json diff --git a/crates/context-aware-config/samples/overrides.json b/crates/context_aware_config/samples/overrides.json similarity index 100% rename from crates/context-aware-config/samples/overrides.json rename to crates/context_aware_config/samples/overrides.json diff --git a/crates/context-aware-config/src/api/audit_log/handlers.rs b/crates/context_aware_config/src/api/audit_log/handlers.rs similarity index 100% rename from crates/context-aware-config/src/api/audit_log/handlers.rs rename to crates/context_aware_config/src/api/audit_log/handlers.rs diff --git a/crates/context-aware-config/src/api/audit_log/mod.rs b/crates/context_aware_config/src/api/audit_log/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/audit_log/mod.rs rename to crates/context_aware_config/src/api/audit_log/mod.rs diff --git a/crates/context-aware-config/src/api/audit_log/types.rs b/crates/context_aware_config/src/api/audit_log/types.rs similarity index 100% rename from crates/context-aware-config/src/api/audit_log/types.rs rename to crates/context_aware_config/src/api/audit_log/types.rs diff --git a/crates/context-aware-config/src/api/config/handlers.rs b/crates/context_aware_config/src/api/config/handlers.rs similarity index 98% rename from crates/context-aware-config/src/api/config/handlers.rs rename to crates/context_aware_config/src/api/config/handlers.rs index a25f0f26f..73b6bc2e4 100644 --- a/crates/context-aware-config/src/api/config/handlers.rs +++ b/crates/context_aware_config/src/api/config/handlers.rs @@ -12,7 +12,7 @@ use crate::db::schema::{ use actix_http::header::{HeaderName, HeaderValue}; use actix_web::{get, web::Query, HttpRequest, HttpResponse, Scope}; use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy}; -use chrono::{DateTime, NaiveDateTime, Timelike, Utc}; +use chrono::{DateTime, NaiveDateTime, TimeZone, Timelike, Utc}; use diesel::{ dsl::max, r2d2::{ConnectionManager, PooledConnection}, @@ -62,7 +62,7 @@ fn add_last_modified_header( let header_name = HeaderName::from_static("last-modified"); if let Some(ele) = max_created_at { - let datetime_utc: DateTime<Utc> = DateTime::from_utc(ele, Utc); + let datetime_utc: DateTime<Utc> = TimeZone::from_utc_datetime(&Utc, &ele); let value = HeaderValue::from_str(&DateTime::to_rfc2822(&datetime_utc)); if let Ok(header_value) = value { res.headers_mut().insert(header_name, header_value); diff --git a/crates/context-aware-config/src/api/config/helpers.rs b/crates/context_aware_config/src/api/config/helpers.rs similarity index 100% rename from crates/context-aware-config/src/api/config/helpers.rs rename to crates/context_aware_config/src/api/config/helpers.rs diff --git a/crates/context-aware-config/src/api/config/mod.rs b/crates/context_aware_config/src/api/config/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/config/mod.rs rename to crates/context_aware_config/src/api/config/mod.rs diff --git a/crates/context-aware-config/src/api/config/types.rs b/crates/context_aware_config/src/api/config/types.rs similarity index 100% rename from crates/context-aware-config/src/api/config/types.rs rename to crates/context_aware_config/src/api/config/types.rs diff --git a/crates/context-aware-config/src/api/context/handlers.rs b/crates/context_aware_config/src/api/context/handlers.rs similarity index 96% rename from crates/context-aware-config/src/api/context/handlers.rs rename to crates/context_aware_config/src/api/context/handlers.rs index 3a0dbfc0b..df7f0fe34 100644 --- a/crates/context-aware-config/src/api/context/handlers.rs +++ b/crates/context_aware_config/src/api/context/handlers.rs @@ -24,7 +24,6 @@ use actix_web::{ HttpResponse, Responder, Scope, }; use chrono::Utc; -use dashboard_auth::types::User; use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, @@ -36,6 +35,7 @@ use serde_json::{from_value, json, Map, Value}; use service_utils::service::types::DbConnection; use service_utils::{db_error, not_found, unexpected_error, validation_error}; use std::collections::HashMap; +use superposition_types::{SuperpositionUser, User}; use super::helpers::{ validate_condition_with_functions, validate_override_with_functions, @@ -205,7 +205,7 @@ fn create_ctx_from_put_req( override_id: override_id.to_owned(), override_: ctx_override.to_owned(), created_at: Utc::now(), - created_by: user.email.clone(), + created_by: user.get_email(), }) } @@ -247,12 +247,11 @@ fn get_put_resp(ctx: Context) -> PutResp { fn put( req: Json<PutReq>, - user: &User, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, already_under_txn: bool, + user: &User, ) -> superposition::Result<PutResp> { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, user)?; if already_under_txn { @@ -278,10 +277,10 @@ fn put( #[put("")] async fn put_handler( req: Json<PutReq>, - user: User, mut db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<PutResp>> { - put(req, &user, &mut db_conn, false) + put(req, &mut db_conn, false, &user) .map(|resp| Json(resp)) .map_err(|err: superposition::AppError| { log::info!("context put failed with error: {:?}", err); @@ -292,9 +291,9 @@ async fn put_handler( fn r#move( old_ctx_id: String, req: Json<MoveReq>, - user: &User, conn: &mut PooledConnection<ConnectionManager<PgConnection>>, already_under_txn: bool, + user: &User, ) -> superposition::Result<PutResp> { use contexts::dsl; let req = req.into_inner(); @@ -329,7 +328,7 @@ fn r#move( value: ctx_condition, priority, created_at: Utc::now(), - created_by: user.email.clone(), + created_by: user.get_email(), override_id: ctx.override_id, override_: ctx.override_, }; @@ -373,10 +372,10 @@ fn r#move( async fn move_handler( path: Path<String>, req: Json<MoveReq>, - user: User, mut db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<PutResp>> { - r#move(path.into_inner(), req, &user, &mut db_conn, false) + r#move(path.into_inner(), req, &mut db_conn, false, &user) .map(|resp| Json(resp)) .map_err(|err| { log::info!("move api failed with error: {:?}", err); @@ -436,8 +435,8 @@ async fn list_contexts( #[delete("/{ctx_id}")] async fn delete_context( path: Path<String>, - user: User, db_conn: DbConnection, + user: User, ) -> superposition::Result<HttpResponse> { use contexts::dsl; let DbConnection(mut conn) = db_conn; @@ -448,7 +447,7 @@ async fn delete_context( match deleted_row { Ok(0) => Err(not_found!("Context Id `{}` doesn't exists", ctx_id)), Ok(_) => { - log::info!("{ctx_id} context deleted by {}", user.email); + log::info!("{ctx_id} context deleted by {}", user.get_email()); Ok(HttpResponse::NoContent().finish()) } Err(e) => { @@ -461,8 +460,8 @@ async fn delete_context( #[put("/bulk-operations")] async fn bulk_operations( reqs: Json<Vec<ContextAction>>, - user: User, db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<Vec<ContextBulkResponse>>> { use contexts::dsl::contexts; let DbConnection(mut conn) = db_conn; @@ -472,7 +471,7 @@ async fn bulk_operations( for action in reqs.into_inner().into_iter() { match action { ContextAction::PUT(put_req) => { - let put_resp = put(Json(put_req), &user, transaction_conn, true) + let put_resp = put(Json(put_req), transaction_conn, true, &user) .map_err(|err| { log::error!( "Failed at insert into contexts due to {:?}", @@ -485,7 +484,7 @@ async fn bulk_operations( ContextAction::DELETE(ctx_id) => { let deleted_row = delete(contexts.filter(id.eq(&ctx_id))).execute(transaction_conn); - let email = user.clone().email; + let email: String = user.get_email(); match deleted_row { // Any kind of error would rollback the tranction but explicitly returning rollback tranction allows you to rollback from any point in transaction. Ok(0) => { @@ -508,7 +507,7 @@ async fn bulk_operations( } ContextAction::MOVE((old_ctx_id, move_req)) => { let move_context_resp = - r#move(old_ctx_id, Json(move_req), &user, transaction_conn, true) + r#move(old_ctx_id, Json(move_req), transaction_conn, true, &user) .map_err(|err| { log::error!( "Failed at moving context reponse due to {:?}", diff --git a/crates/context-aware-config/src/api/context/helpers.rs b/crates/context_aware_config/src/api/context/helpers.rs similarity index 100% rename from crates/context-aware-config/src/api/context/helpers.rs rename to crates/context_aware_config/src/api/context/helpers.rs diff --git a/crates/context-aware-config/src/api/context/mod.rs b/crates/context_aware_config/src/api/context/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/context/mod.rs rename to crates/context_aware_config/src/api/context/mod.rs diff --git a/crates/context-aware-config/src/api/context/types.rs b/crates/context_aware_config/src/api/context/types.rs similarity index 100% rename from crates/context-aware-config/src/api/context/types.rs rename to crates/context_aware_config/src/api/context/types.rs diff --git a/crates/context-aware-config/src/api/default_config/handlers.rs b/crates/context_aware_config/src/api/default_config/handlers.rs similarity index 98% rename from crates/context-aware-config/src/api/default_config/handlers.rs rename to crates/context_aware_config/src/api/default_config/handlers.rs index ed264c624..6f2755cb7 100644 --- a/crates/context-aware-config/src/api/default_config/handlers.rs +++ b/crates/context_aware_config/src/api/default_config/handlers.rs @@ -2,6 +2,8 @@ extern crate base64; use super::types::CreateReq; use service_utils::{bad_argument, unexpected_error, validation_error}; +use superposition_types::{SuperpositionUser, User}; + use crate::api::context::helpers::validate_value_with_function; use crate::{ api::functions::helpers::get_published_function_code, @@ -14,7 +16,6 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::types::User; use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, @@ -35,8 +36,8 @@ async fn create( state: Data<AppState>, key: web::Path<String>, request: web::Json<CreateReq>, - user: User, db_conn: DbConnection, + user: User, ) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); @@ -90,7 +91,7 @@ async fn create( value, schema, function_name, - created_by: user.email, + created_by: user.get_email(), created_at: Utc::now(), }; diff --git a/crates/context-aware-config/src/api/default_config/mod.rs b/crates/context_aware_config/src/api/default_config/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/default_config/mod.rs rename to crates/context_aware_config/src/api/default_config/mod.rs diff --git a/crates/context-aware-config/src/api/default_config/types.rs b/crates/context_aware_config/src/api/default_config/types.rs similarity index 100% rename from crates/context-aware-config/src/api/default_config/types.rs rename to crates/context_aware_config/src/api/default_config/types.rs diff --git a/crates/context-aware-config/src/api/dimension/handlers.rs b/crates/context_aware_config/src/api/dimension/handlers.rs similarity index 95% rename from crates/context-aware-config/src/api/dimension/handlers.rs rename to crates/context_aware_config/src/api/dimension/handlers.rs index 859e4a3a9..0092be472 100644 --- a/crates/context-aware-config/src/api/dimension/handlers.rs +++ b/crates/context_aware_config/src/api/dimension/handlers.rs @@ -9,7 +9,6 @@ use actix_web::{ HttpResponse, Scope, }; use chrono::Utc; -use dashboard_auth::types::User; use diesel::RunQueryDsl; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; @@ -19,6 +18,8 @@ use service_utils::{ unexpected_error, }; +use superposition_types::{SuperpositionUser, User}; + pub fn endpoints() -> Scope { Scope::new("").service(create).service(get) } @@ -30,7 +31,6 @@ async fn create( user: User, db_conn: DbConnection, ) -> superposition::Result<HttpResponse> { - //TODO move this to the type itself rather than special if check let DbConnection(mut conn) = db_conn; if req.priority <= 0 { @@ -68,7 +68,7 @@ async fn create( dimension: create_req.dimension, priority: i32::from(create_req.priority), schema: schema_value, - created_by: user.email, + created_by: user.get_email(), created_at: Utc::now(), function_name: fun_name.clone(), }; diff --git a/crates/context-aware-config/src/api/dimension/mod.rs b/crates/context_aware_config/src/api/dimension/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/dimension/mod.rs rename to crates/context_aware_config/src/api/dimension/mod.rs diff --git a/crates/context-aware-config/src/api/dimension/types.rs b/crates/context_aware_config/src/api/dimension/types.rs similarity index 100% rename from crates/context-aware-config/src/api/dimension/types.rs rename to crates/context_aware_config/src/api/dimension/types.rs diff --git a/crates/context-aware-config/src/api/dimension/utils.rs b/crates/context_aware_config/src/api/dimension/utils.rs similarity index 100% rename from crates/context-aware-config/src/api/dimension/utils.rs rename to crates/context_aware_config/src/api/dimension/utils.rs diff --git a/crates/context-aware-config/src/api/functions/handlers.rs b/crates/context_aware_config/src/api/functions/handlers.rs similarity index 97% rename from crates/context-aware-config/src/api/functions/handlers.rs rename to crates/context_aware_config/src/api/functions/handlers.rs index f045cc69e..5322f231d 100644 --- a/crates/context-aware-config/src/api/functions/handlers.rs +++ b/crates/context_aware_config/src/api/functions/handlers.rs @@ -18,10 +18,12 @@ use actix_web::{ HttpResponse, Result, Scope, }; use chrono::Utc; -use dashboard_auth::types::User; use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json::json; use service_utils::{bad_argument, not_found, service::types::DbConnection}; + +use superposition_types::{SuperpositionUser, User}; + use service_utils::{result as superposition, unexpected_error}; use validation_functions::{compile_fn, execute_fn}; @@ -40,9 +42,9 @@ pub fn endpoints() -> Scope { #[post("")] async fn create( - user: User, request: web::Json<CreateFunctionRequest>, db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); @@ -53,7 +55,7 @@ async fn create( function_name: req.function_name, draft_code: BASE64_STANDARD.encode(req.function), draft_runtime_version: req.runtime_version, - draft_edited_by: user.email, + draft_edited_by: user.get_email(), draft_edited_at: Utc::now().naive_utc(), published_code: None, published_at: None, @@ -97,10 +99,10 @@ async fn create( #[patch("/{function_name}")] async fn update( - user: User, params: web::Path<String>, request: web::Json<UpdateFunctionRequest>, db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); @@ -133,7 +135,7 @@ async fn update( .runtime_version .unwrap_or(result.draft_runtime_version), function_description: req.description.unwrap_or(result.function_description), - draft_edited_by: user.email, + draft_edited_by: user.get_email(), draft_edited_at: Utc::now().naive_utc(), published_code: result.published_code, published_at: result.published_at, @@ -177,9 +179,9 @@ async fn list_functions( #[delete("/{function_name}")] async fn delete_function( - user: User, params: web::Path<String>, db_conn: DbConnection, + user: User, ) -> superposition::Result<HttpResponse> { let DbConnection(mut conn) = db_conn; let f_name = params.into_inner(); @@ -189,7 +191,7 @@ async fn delete_function( match deleted_row { Ok(0) => Err(not_found!("Function {} doesn't exists", f_name)), Ok(_) => { - log::info!("{f_name} function deleted by {}", user.email); + log::info!("{f_name} function deleted by {}", user.get_email()); Ok(HttpResponse::NoContent().finish()) } Err(e) => { @@ -249,9 +251,9 @@ async fn test( #[put("/{function_name}/publish")] async fn publish( - user: User, params: web::Path<String>, db_conn: DbConnection, + user: User, ) -> superposition::Result<Json<Function>> { let DbConnection(mut conn) = db_conn; let fun_name = params.into_inner(); @@ -276,7 +278,7 @@ async fn publish( dsl::published_code.eq(Some(function.draft_code.clone())), dsl::published_runtime_version .eq(Some(function.draft_runtime_version.clone())), - dsl::published_by.eq(Some(user.email)), + dsl::published_by.eq(Some(user.get_email())), dsl::published_at.eq(Some(Utc::now().naive_utc())), )) .get_result::<Function>(&mut conn)?; diff --git a/crates/context-aware-config/src/api/functions/helpers.rs b/crates/context_aware_config/src/api/functions/helpers.rs similarity index 100% rename from crates/context-aware-config/src/api/functions/helpers.rs rename to crates/context_aware_config/src/api/functions/helpers.rs diff --git a/crates/context-aware-config/src/api/functions/mod.rs b/crates/context_aware_config/src/api/functions/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/functions/mod.rs rename to crates/context_aware_config/src/api/functions/mod.rs diff --git a/crates/context-aware-config/src/api/functions/types.rs b/crates/context_aware_config/src/api/functions/types.rs similarity index 100% rename from crates/context-aware-config/src/api/functions/types.rs rename to crates/context_aware_config/src/api/functions/types.rs diff --git a/crates/context-aware-config/src/api/mod.rs b/crates/context_aware_config/src/api/mod.rs similarity index 100% rename from crates/context-aware-config/src/api/mod.rs rename to crates/context_aware_config/src/api/mod.rs diff --git a/crates/context-aware-config/src/db/mod.rs b/crates/context_aware_config/src/db/mod.rs similarity index 100% rename from crates/context-aware-config/src/db/mod.rs rename to crates/context_aware_config/src/db/mod.rs diff --git a/crates/context-aware-config/src/db/models.rs b/crates/context_aware_config/src/db/models.rs similarity index 100% rename from crates/context-aware-config/src/db/models.rs rename to crates/context_aware_config/src/db/models.rs diff --git a/crates/context-aware-config/src/db/schema.rs b/crates/context_aware_config/src/db/schema.rs similarity index 100% rename from crates/context-aware-config/src/db/schema.rs rename to crates/context_aware_config/src/db/schema.rs diff --git a/crates/context-aware-config/src/helpers.rs b/crates/context_aware_config/src/helpers.rs similarity index 100% rename from crates/context-aware-config/src/helpers.rs rename to crates/context_aware_config/src/helpers.rs diff --git a/crates/context-aware-config/src/lib.rs b/crates/context_aware_config/src/lib.rs similarity index 100% rename from crates/context-aware-config/src/lib.rs rename to crates/context_aware_config/src/lib.rs diff --git a/crates/context-aware-config/src/middlewares/cookie_to_header.rs b/crates/context_aware_config/src/middlewares/cookie_to_header.rs similarity index 100% rename from crates/context-aware-config/src/middlewares/cookie_to_header.rs rename to crates/context_aware_config/src/middlewares/cookie_to_header.rs diff --git a/crates/context_aware_config/src/middlewares/mod.rs b/crates/context_aware_config/src/middlewares/mod.rs new file mode 100644 index 000000000..842b49c6b --- /dev/null +++ b/crates/context_aware_config/src/middlewares/mod.rs @@ -0,0 +1 @@ +pub mod cookie_to_header; diff --git a/crates/context-aware-config/src/validation_functions.rs b/crates/context_aware_config/src/validation_functions.rs similarity index 100% rename from crates/context-aware-config/src/validation_functions.rs rename to crates/context_aware_config/src/validation_functions.rs diff --git a/crates/context-aware-config/tests/cac_tests.rs b/crates/context_aware_config/tests/cac_tests.rs similarity index 100% rename from crates/context-aware-config/tests/cac_tests.rs rename to crates/context_aware_config/tests/cac_tests.rs diff --git a/crates/context-aware-config/tests/index.js b/crates/context_aware_config/tests/index.js similarity index 100% rename from crates/context-aware-config/tests/index.js rename to crates/context_aware_config/tests/index.js diff --git a/crates/experimentation-platform/CHANGELOG.md b/crates/experimentation_platform/CHANGELOG.md similarity index 100% rename from crates/experimentation-platform/CHANGELOG.md rename to crates/experimentation_platform/CHANGELOG.md diff --git a/crates/experimentation-platform/Cargo.toml b/crates/experimentation_platform/Cargo.toml similarity index 81% rename from crates/experimentation-platform/Cargo.toml rename to crates/experimentation_platform/Cargo.toml index ffe615ced..31597c740 100644 --- a/crates/experimentation-platform/Cargo.toml +++ b/crates/experimentation_platform/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "experimentation-platform" +name = "experimentation_platform" version = "0.12.0" edition = "2021" @@ -28,7 +28,7 @@ chrono = { workspace = true } # ORM diesel = { workspace = true } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } -service-utils = { path = "../service-utils" } +service_utils = { path = "../service_utils" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } reqwest = { workspace = true } -dashboard-auth = { workspace = true } anyhow = { workspace = true } \ No newline at end of file diff --git a/crates/experimentation-platform/diesel.toml b/crates/experimentation_platform/diesel.toml similarity index 100% rename from crates/experimentation-platform/diesel.toml rename to crates/experimentation_platform/diesel.toml diff --git a/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/down.sql rename to crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from crates/experimentation-platform/migrations/00000000000000_diesel_initial_setup/up.sql rename to crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql diff --git a/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/down.sql similarity index 100% rename from crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/down.sql rename to crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/down.sql diff --git a/crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql similarity index 100% rename from crates/experimentation-platform/migrations/2023-10-16-134612_experimentation-init/up.sql rename to crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql diff --git a/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql similarity index 100% rename from crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql rename to crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql diff --git a/crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql similarity index 100% rename from crates/experimentation-platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql rename to crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql diff --git a/crates/experimentation-platform/src/api/experiments/handlers.rs b/crates/experimentation_platform/src/api/experiments/handlers.rs similarity index 97% rename from crates/experimentation-platform/src/api/experiments/handlers.rs rename to crates/experimentation_platform/src/api/experiments/handlers.rs index d2b29fbd9..df77dfb93 100644 --- a/crates/experimentation-platform/src/api/experiments/handlers.rs +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -6,7 +6,6 @@ use actix_web::{ HttpRequest, HttpResponse, Scope, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use dashboard_auth::types::User; use diesel::{ r2d2::{ConnectionManager, PooledConnection}, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, @@ -16,6 +15,8 @@ use service_utils::{ bad_argument, response_error, result as superposition, unexpected_error, }; +use superposition_types::{SuperpositionUser, User}; + use reqwest::{Response, StatusCode}; use service_utils::service::types::{AppState, DbConnection, Tenant}; @@ -99,13 +100,12 @@ async fn process_cac_http_response( async fn create( state: Data<AppState>, req: web::Json<ExperimentCreateRequest>, - user: User, db_conn: DbConnection, tenant: Tenant, + user: User, ) -> superposition::Result<Json<ExperimentCreateResponse>> { use crate::db::schema::experiments::dsl::experiments; let mut variants = req.variants.to_vec(); - let DbConnection(mut conn) = db_conn; // Checking if experiment has exactly 1 control variant, and @@ -195,11 +195,11 @@ async fn create( // Step 1: Perform the HTTP request and handle errors let response = http_client .put(&url) + .header("x-tenant", tenant.as_str()) .header( "Authorization", - format!("{} {}", user.auth_type, user.token), + format!("{} {}", user.get_auth_type(), user.get_auth_token()), ) - .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() .await; @@ -225,7 +225,7 @@ async fn create( // inserting experiment in db let new_experiment = Experiment { id: experiment_id, - created_by: user.email.to_string(), + created_by: user.get_email(), created_at: Utc::now(), last_modified: Utc::now(), name: req.name.to_string(), @@ -234,7 +234,7 @@ async fn create( status: ExperimentStatusType::CREATED, context: req.context.clone(), variants: serde_json::to_value(variants).unwrap(), - last_modified_by: user.email, + last_modified_by: user.get_email(), chosen_variant: None, }; @@ -254,8 +254,8 @@ async fn conclude_handler( path: web::Path<i64>, req: web::Json<ConcludeExperimentRequest>, db_conn: DbConnection, - user: User, tenant: Tenant, + user: User, ) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(conn) = db_conn; let response = conclude( @@ -263,8 +263,8 @@ async fn conclude_handler( path.into_inner(), req.into_inner(), conn, - user, tenant, + user, ) .await?; return Ok(Json(ExperimentResponse::from(response))); @@ -275,8 +275,8 @@ pub async fn conclude( experiment_id: i64, req: ConcludeExperimentRequest, mut conn: PooledConnection<ConnectionManager<PgConnection>>, - user: User, tenant: Tenant, + user: User, ) -> superposition::Result<Experiment> { use crate::db::schema::experiments::dsl; @@ -340,11 +340,11 @@ pub async fn conclude( let url = state.cac_host.clone() + "/context/bulk-operations"; let response = http_client .put(&url) + .header("x-tenant", tenant.as_str()) .header( "Authorization", - format!("{} {}", user.auth_type, user.token), + format!("{} {}", user.get_auth_type(), user.get_auth_token()), ) - .header("x-tenant", tenant.as_str()) .json(&operations) .send() .await; @@ -357,7 +357,7 @@ pub async fn conclude( .set(( dsl::status.eq(ExperimentStatusType::CONCLUDED), dsl::last_modified.eq(Utc::now()), - dsl::last_modified_by.eq(user.email), + dsl::last_modified_by.eq(user.get_email()), dsl::chosen_variant.eq(Some(winner_variant_id)), )) .get_result::<Experiment>(&mut conn)?; @@ -499,7 +499,7 @@ async fn ramp( .set(( experiments::traffic_percentage.eq(req.traffic_percentage as i32), experiments::last_modified.eq(Utc::now()), - experiments::last_modified_by.eq(user.email), + experiments::last_modified_by.eq(user.get_email()), experiments::status.eq(ExperimentStatusType::INPROGRESS), )) .get_result(&mut conn)?; @@ -512,9 +512,9 @@ async fn update_overrides( params: web::Path<i64>, state: Data<AppState>, db_conn: DbConnection, - user: User, req: web::Json<OverrideKeysUpdateRequest>, tenant: Tenant, + user: User, ) -> superposition::Result<Json<ExperimentResponse>> { let DbConnection(mut conn) = db_conn; let experiment_id = params.into_inner(); @@ -657,11 +657,11 @@ async fn update_overrides( let response = http_client .put(&url) + .header("x-tenant", tenant.as_str()) .header( "Authorization", - format!("{} {}", user.auth_type, user.token), + format!("{} {}", user.get_auth_type(), user.get_auth_token()), ) - .header("x-tenant", tenant.as_str()) .json(&cac_operations) .send() .await; @@ -695,7 +695,7 @@ async fn update_overrides( experiments::variants.eq(new_variants_json), experiments::override_keys.eq(override_keys), experiments::last_modified.eq(Utc::now()), - experiments::last_modified_by.eq(user.email), + experiments::last_modified_by.eq(user.get_email()), )) .get_result::<Experiment>(&mut conn)?; diff --git a/crates/experimentation-platform/src/api/experiments/helpers.rs b/crates/experimentation_platform/src/api/experiments/helpers.rs similarity index 100% rename from crates/experimentation-platform/src/api/experiments/helpers.rs rename to crates/experimentation_platform/src/api/experiments/helpers.rs diff --git a/crates/experimentation-platform/src/api/experiments/mod.rs b/crates/experimentation_platform/src/api/experiments/mod.rs similarity index 100% rename from crates/experimentation-platform/src/api/experiments/mod.rs rename to crates/experimentation_platform/src/api/experiments/mod.rs diff --git a/crates/experimentation-platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs similarity index 100% rename from crates/experimentation-platform/src/api/experiments/types.rs rename to crates/experimentation_platform/src/api/experiments/types.rs diff --git a/crates/experimentation-platform/src/api/mod.rs b/crates/experimentation_platform/src/api/mod.rs similarity index 100% rename from crates/experimentation-platform/src/api/mod.rs rename to crates/experimentation_platform/src/api/mod.rs diff --git a/crates/experimentation-platform/src/db/mod.rs b/crates/experimentation_platform/src/db/mod.rs similarity index 100% rename from crates/experimentation-platform/src/db/mod.rs rename to crates/experimentation_platform/src/db/mod.rs diff --git a/crates/experimentation-platform/src/db/models.rs b/crates/experimentation_platform/src/db/models.rs similarity index 100% rename from crates/experimentation-platform/src/db/models.rs rename to crates/experimentation_platform/src/db/models.rs diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation_platform/src/db/schema.rs similarity index 100% rename from crates/experimentation-platform/src/db/schema.rs rename to crates/experimentation_platform/src/db/schema.rs diff --git a/crates/experimentation-platform/src/lib.rs b/crates/experimentation_platform/src/lib.rs similarity index 100% rename from crates/experimentation-platform/src/lib.rs rename to crates/experimentation_platform/src/lib.rs diff --git a/crates/experimentation-platform/src/schema.patch b/crates/experimentation_platform/src/schema.patch similarity index 100% rename from crates/experimentation-platform/src/schema.patch rename to crates/experimentation_platform/src/schema.patch diff --git a/crates/experimentation-platform/tests/experimentation_tests.rs b/crates/experimentation_platform/tests/experimentation_tests.rs similarity index 100% rename from crates/experimentation-platform/tests/experimentation_tests.rs rename to crates/experimentation_platform/tests/experimentation_tests.rs diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml index f568abdfe..60f971bcb 100644 --- a/crates/external/Cargo.toml +++ b/crates/external/Cargo.toml @@ -19,8 +19,9 @@ log = { workspace = true } # date and time chrono = { workspace = true } # ORM -service-utils = { path = "../service-utils" } reqwest = { workspace = true } -experimentation-platform = { path = "../experimentation-platform"} -dashboard-auth = { workspace = true } -anyhow = { workspace = true } \ No newline at end of file +anyhow = { workspace = true } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.9.0" } diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs index e19180af8..44ad8089d 100644 --- a/crates/external/src/api/external_api/handlers.rs +++ b/crates/external/src/api/external_api/handlers.rs @@ -3,7 +3,6 @@ use actix_web::{ web::{self, Data, Json}, Scope, }; -use dashboard_auth::types::User; use crate::api::external_api::{ helpers::{fetch_variant_id, get_resolved_config}, @@ -23,6 +22,8 @@ use service_utils::{ service::types::{AppState, DbConnection, Tenant}, }; +use superposition_types::User; + pub fn endpoints(scope: Scope) -> Scope { scope .service(stabilize) @@ -35,16 +36,16 @@ async fn stabilize( params: web::Path<i64>, state: Data<AppState>, db_conn: DbConnection, - user: User, tenant: Tenant, + user: User, ) -> result::Result<Json<ExperimentResponse>> { let response = conclude_experiment( params.into_inner(), state, db_conn, - user, tenant, VariantType::EXPERIMENTAL, + user, ) .await?; return Ok(Json(ExperimentResponse::from(response))); @@ -55,16 +56,16 @@ async fn revert( params: web::Path<i64>, state: Data<AppState>, db_conn: DbConnection, - user: User, tenant: Tenant, + user: User, ) -> result::Result<Json<ExperimentResponse>> { let response = conclude_experiment( params.into_inner(), state, db_conn, - user, tenant, VariantType::CONTROL, + user, ) .await?; return Ok(Json(ExperimentResponse::from(response))); @@ -74,9 +75,9 @@ pub async fn conclude_experiment( exp_id: i64, state: Data<AppState>, db_conn: DbConnection, - user: User, tenant: Tenant, variant: VariantType, + user: User, ) -> result::Result<Experiment> { let DbConnection(mut conn) = db_conn; @@ -85,7 +86,7 @@ pub async fn conclude_experiment( let req_body = ConcludeExperimentRequest { chosen_variant: id.to_string(), }; - let response = conclude(state, exp_id, req_body, conn, user, tenant).await?; + let response = conclude(state, exp_id, req_body, conn, tenant, user).await?; return Ok(response); } diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs index 7d19c57d9..9881abdc8 100644 --- a/crates/external/src/api/external_api/helpers.rs +++ b/crates/external/src/api/external_api/helpers.rs @@ -39,7 +39,6 @@ pub async fn get_resolved_config( let url = format!("{}/config/resolve", state.cac_host); let resp = http_client .get(&url) - .bearer_auth(&state.admin_token) .header("x-tenant", "mjos") .query(dimension_ctx) .send() diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index f78104d86..f68d30c54 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,10 +1,8 @@ use crate::types::Dimension; use crate::utils::{get_config_value, get_host, ConfigType}; -use anyhow::{Error, Result}; +use anyhow::Result; use reqwest::StatusCode; -use serde_json::{json, Map, Number, Value}; -use std::io::ErrorKind; -use std::str::FromStr; +use serde_json::{json, Map, Value}; pub fn get_condition_schema( var: &str, diff --git a/crates/frontend/src/components/function_form/function_form.rs b/crates/frontend/src/components/function_form/function_form.rs index 4da400b00..cfcb36f3c 100644 --- a/crates/frontend/src/components/function_form/function_form.rs +++ b/crates/frontend/src/components/function_form/function_form.rs @@ -257,7 +257,7 @@ where set_error_message.set("".to_string()); set_output_message.set("".to_string()); } - Err(e) => { + Err(_) => { set_val.set(json!(value)); set_error_message.set("".to_string()); set_output_message.set("".to_string()); diff --git a/crates/frontend/src/pages/function/function_list.rs b/crates/frontend/src/pages/function/function_list.rs index dc524fa56..24ec320ea 100644 --- a/crates/frontend/src/pages/function/function_list.rs +++ b/crates/frontend/src/pages/function/function_list.rs @@ -32,18 +32,17 @@ pub fn function_list() -> impl IntoView { }); let table_columns = create_memo(move |_| function_table_columns()); - let combined_resource: Resource<(String), CombinedResource> = - create_blocking_resource( - move || (tenant_rs.get()), - |(current_tenant)| async move { - let functions_future = fetch_functions(current_tenant.to_string()); - - let functions_result = functions_future.await; - CombinedResource { - functions: functions_result.unwrap_or_else(|_| vec![]), - } - }, - ); + let combined_resource: Resource<String, CombinedResource> = create_blocking_resource( + move || (tenant_rs.get()), + |current_tenant| async move { + let functions_future = fetch_functions(current_tenant.to_string()); + + let functions_result = functions_future.await; + CombinedResource { + functions: functions_result.unwrap_or_else(|_| vec![]), + } + }, + ); view! { <div class="p-8"> diff --git a/crates/juspay_superposition/Cargo.toml b/crates/juspay_superposition/Cargo.toml new file mode 100644 index 000000000..c3a2e7225 --- /dev/null +++ b/crates/juspay_superposition/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "juspay_superposition" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cac_client = { path = "../cac_client" } +frontend = { path = "../frontend" } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +context_aware_config = { path = "../context_aware_config" } +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.6.0" } +tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.3.0" } +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +strum_macros = { workspace = true } +strum = { workspace = true } +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +blake3 = { workspace = true} +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +rand = { workspace = true } +tracing-log = "0.1.3" +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" +futures = "0.3.28" +actix-http = "3.3.1" +futures-util = "0.3.28" +external = { path = "../external" } +actix-cors = "0.6.4" +leptos_actix = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +actix-files = { version = "0.6" } +anyhow = { workspace = true } + diff --git a/crates/context-aware-config/src/auth.rs b/crates/juspay_superposition/src/auth.rs similarity index 100% rename from crates/context-aware-config/src/auth.rs rename to crates/juspay_superposition/src/auth.rs diff --git a/crates/context-aware-config/src/logger.rs b/crates/juspay_superposition/src/logger.rs similarity index 100% rename from crates/context-aware-config/src/logger.rs rename to crates/juspay_superposition/src/logger.rs diff --git a/crates/context-aware-config/src/main.rs b/crates/juspay_superposition/src/main.rs similarity index 95% rename from crates/context-aware-config/src/main.rs rename to crates/juspay_superposition/src/main.rs index be88892fd..c16e6faf6 100644 --- a/crates/context-aware-config/src/main.rs +++ b/crates/juspay_superposition/src/main.rs @@ -1,24 +1,25 @@ -mod api; mod auth; -mod db; -mod helpers; mod logger; mod middlewares; -mod validation_functions; -use crate::middlewares::cookie_to_header::CookieToHeader; -use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; -use api::*; +use actix_web::{ + web::{self, get, scope, Data}, + App, HttpResponse, HttpServer, +}; use auth::fill_service_prefix; +use context_aware_config::api::*; +use context_aware_config::helpers::{ + get_default_config_validation_schema, get_meta_schema, +}; +use context_aware_config::middlewares::cookie_to_header::CookieToHeader; use dashboard_auth::{ middleware::DashboardAuth, types::{AuthenticatedRoute, AuthenticatedRouteList}, }; use dotenv; use experimentation_platform::api::*; -use helpers::{get_default_config_validation_schema, get_meta_schema}; use logger::{init_log_subscriber, CustomRootSpanBuilder}; -use std::{collections::HashSet, env, io::Result}; +use std::{collections::HashSet, io::Result}; use tracing::{span, Level}; use snowflake::SnowflakeIdGenerator; @@ -31,8 +32,7 @@ use frontend::types::Envs as UIEnvs; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use service_utils::{ - db::pgschema_manager::PgSchemaManager, - db::utils::init_pool_manager, + db::{pgschema_manager::PgSchemaManager, utils::init_pool_manager}, helpers::{get_from_env_or_default, get_from_env_unsafe, get_pod_info}, middlewares::{ app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, @@ -84,7 +84,7 @@ async fn main() -> Result<()> { prefix => "/".to_owned() + prefix, }; - let admin_token = env::var("ADMIN_TOKEN").expect("Admin token is not set!"); + let cac_port: u16 = get_from_env_unsafe("PORT").unwrap_or(8080); let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); @@ -165,12 +165,11 @@ async fn main() -> Result<()> { .wrap(DashboardAuth::default(authenticated_routes(""))) .wrap(TenantMiddlewareFactory) .wrap(middlewares::cors()) - .wrap(GoldenSignalFactory) + .wrap(GoldenSignalFactory::with_blacklist(vec![])) .wrap(TracingLogger::<CustomRootSpanBuilder>::new()) .app_data(Data::new(AppState { db_pool: schema_manager.clone(), default_config_validation_schema: get_default_config_validation_schema(), - admin_token: admin_token.to_owned(), cac_host: cac_host.to_owned(), cac_version: cac_version.to_owned(), @@ -231,7 +230,7 @@ async fn main() -> Result<()> { .service(default_config::endpoints()), ) .service( - scope("/config") + scope("/config") .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) .service(config::endpoints()), ) @@ -300,7 +299,7 @@ async fn main() -> Result<()> { ) .app_data(Data::new(leptos_options.to_owned())) }) - .bind(("0.0.0.0", 8080))? + .bind(("0.0.0.0", cac_port))? .workers(5) .keep_alive(Duration::from_secs( get_from_env_unsafe("ACTIX_KEEP_ALIVE").unwrap_or(120), diff --git a/crates/context-aware-config/src/middlewares/mod.rs b/crates/juspay_superposition/src/middlewares/mod.rs similarity index 97% rename from crates/context-aware-config/src/middlewares/mod.rs rename to crates/juspay_superposition/src/middlewares/mod.rs index e1b697d54..7c8f2b34c 100644 --- a/crates/context-aware-config/src/middlewares/mod.rs +++ b/crates/juspay_superposition/src/middlewares/mod.rs @@ -1,5 +1,3 @@ -pub mod cookie_to_header; - use actix_web::{dev::RequestHead, http::header::HeaderValue}; pub fn cors() -> actix_cors::Cors { let origins_env_name = "MJOS_ALLOWED_ORIGINS"; diff --git a/crates/service-utils/CHANGELOG.md b/crates/service_utils/CHANGELOG.md similarity index 83% rename from crates/service-utils/CHANGELOG.md rename to crates/service_utils/CHANGELOG.md index e09f4a1ac..076ad9782 100644 --- a/crates/service-utils/CHANGELOG.md +++ b/crates/service_utils/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - -## service-utils-v0.13.0 - 2024-04-10 +## service_utils-v0.13.0 - 2024-04-10 #### Features - [PICAF-25423] added new result, error type and error macros - (e673fb1) - Shubhranshu Sanjeev #### Refactoring @@ -10,37 +10,37 @@ All notable changes to this project will be documented in this file. See [conven - - - -## service-utils-v0.12.0 - 2024-03-08 +## service_utils-v0.12.0 - 2024-03-08 #### Features - PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato - - - -## service-utils-v0.11.0 - 2024-02-20 +## service_utils-v0.11.0 - 2024-02-20 #### Features - support for service prefix - (a2915b4) - Shubhranshu Sanjeev - - - -## service-utils-v0.10.3 - 2024-02-15 +## service_utils-v0.10.3 - 2024-02-15 #### Bug Fixes - fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins - - - -## service-utils-v0.10.2 - 2024-01-22 +## service_utils-v0.10.2 - 2024-01-22 #### Bug Fixes - fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev - - - -## service-utils-v0.10.1 - 2024-01-18 +## service_utils-v0.10.1 - 2024-01-18 #### Bug Fixes - error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev - - - -## service-utils-v0.10.0 - 2024-01-04 +## service_utils-v0.10.0 - 2024-01-04 #### Bug Fixes - frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev #### Features @@ -48,7 +48,7 @@ All notable changes to this project will be documented in this file. See [conven - - - -## service-utils-v0.9.0 - 2023-11-11 +## service_utils-v0.9.0 - 2023-11-11 #### Features - added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman #### Miscellaneous Chores @@ -56,7 +56,7 @@ All notable changes to this project will be documented in this file. See [conven - - - -## service-utils-v0.8.0 - 2023-11-08 +## service_utils-v0.8.0 - 2023-11-08 #### Bug Fixes - make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra #### Features @@ -64,50 +64,50 @@ All notable changes to this project will be documented in this file. See [conven - - - -## service-utils-v0.7.1 - 2023-10-27 +## service_utils-v0.7.1 - 2023-10-27 #### Bug Fixes - fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev - - - -## service-utils-v0.7.0 - 2023-10-25 +## service_utils-v0.7.0 - 2023-10-25 #### Features - added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev - added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev - - - -## service-utils-v0.6.0 - 2023-10-20 +## service_utils-v0.6.0 - 2023-10-20 #### Features - PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P - - - -## service-utils-v0.5.0 - 2023-10-09 +## service_utils-v0.5.0 - 2023-10-09 #### Features - server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan - - - -## service-utils-v0.4.1 - 2023-10-05 +## service_utils-v0.4.1 - 2023-10-05 #### Bug Fixes - [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra - - - -## service-utils-v0.4.0 - 2023-09-12 +## service_utils-v0.4.0 - 2023-09-12 #### Features - Schema addition for Dimension values - (7960a67) - Prasanna P - - - -## service-utils-v0.3.0 - 2023-09-06 +## service_utils-v0.3.0 - 2023-09-06 #### Features - [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra - - - -## service-utils-v0.2.0 - 2023-09-05 +## service_utils-v0.2.0 - 2023-09-05 #### Features - [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra #### Revert @@ -115,7 +115,7 @@ All notable changes to this project will be documented in this file. See [conven - - - -## service-utils-v0.1.0 - 2023-09-01 +## service_utils-v0.1.0 - 2023-09-01 #### Bug Fixes - PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan - added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev diff --git a/crates/service-utils/Cargo.toml b/crates/service_utils/Cargo.toml similarity index 90% rename from crates/service-utils/Cargo.toml rename to crates/service_utils/Cargo.toml index 93e4d4320..d5411c86d 100644 --- a/crates/service-utils/Cargo.toml +++ b/crates/service_utils/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "service-utils" +name = "service_utils" version = "0.13.0" edition = "2021" @@ -31,6 +31,5 @@ log = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } derive_more = { workspace = true } -dashboard-auth = { workspace = true } reqwest = { workspace = true } -thiserror = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/service-utils/src/aws/kms.rs b/crates/service_utils/src/aws/kms.rs similarity index 100% rename from crates/service-utils/src/aws/kms.rs rename to crates/service_utils/src/aws/kms.rs diff --git a/crates/service-utils/src/aws/mod.rs b/crates/service_utils/src/aws/mod.rs similarity index 100% rename from crates/service-utils/src/aws/mod.rs rename to crates/service_utils/src/aws/mod.rs diff --git a/crates/service-utils/src/db/mod.rs b/crates/service_utils/src/db/mod.rs similarity index 100% rename from crates/service-utils/src/db/mod.rs rename to crates/service_utils/src/db/mod.rs diff --git a/crates/service-utils/src/db/pgschema_manager.rs b/crates/service_utils/src/db/pgschema_manager.rs similarity index 100% rename from crates/service-utils/src/db/pgschema_manager.rs rename to crates/service_utils/src/db/pgschema_manager.rs diff --git a/crates/service-utils/src/db/utils.rs b/crates/service_utils/src/db/utils.rs similarity index 100% rename from crates/service-utils/src/db/utils.rs rename to crates/service_utils/src/db/utils.rs diff --git a/crates/service-utils/src/helpers.rs b/crates/service_utils/src/helpers.rs similarity index 100% rename from crates/service-utils/src/helpers.rs rename to crates/service_utils/src/helpers.rs diff --git a/crates/service-utils/src/lib.rs b/crates/service_utils/src/lib.rs similarity index 100% rename from crates/service-utils/src/lib.rs rename to crates/service_utils/src/lib.rs diff --git a/crates/service-utils/src/macros.rs b/crates/service_utils/src/macros.rs similarity index 100% rename from crates/service-utils/src/macros.rs rename to crates/service_utils/src/macros.rs diff --git a/crates/service-utils/src/middlewares/app_scope.rs b/crates/service_utils/src/middlewares/app_scope.rs similarity index 99% rename from crates/service-utils/src/middlewares/app_scope.rs rename to crates/service_utils/src/middlewares/app_scope.rs index 6f8362ed8..4f6876d0f 100644 --- a/crates/service-utils/src/middlewares/app_scope.rs +++ b/crates/service_utils/src/middlewares/app_scope.rs @@ -63,7 +63,6 @@ where Box::pin(async move { req.extensions_mut().insert(scope); let res = srv.call(req).await?; - Ok(res) }) } diff --git a/crates/service-utils/src/middlewares/mod.rs b/crates/service_utils/src/middlewares/mod.rs similarity index 100% rename from crates/service-utils/src/middlewares/mod.rs rename to crates/service_utils/src/middlewares/mod.rs diff --git a/crates/service-utils/src/middlewares/tenant.rs b/crates/service_utils/src/middlewares/tenant.rs similarity index 100% rename from crates/service-utils/src/middlewares/tenant.rs rename to crates/service_utils/src/middlewares/tenant.rs diff --git a/crates/service-utils/src/result.rs b/crates/service_utils/src/result.rs similarity index 100% rename from crates/service-utils/src/result.rs rename to crates/service_utils/src/result.rs diff --git a/crates/service-utils/src/service/mod.rs b/crates/service_utils/src/service/mod.rs similarity index 100% rename from crates/service-utils/src/service/mod.rs rename to crates/service_utils/src/service/mod.rs diff --git a/crates/service-utils/src/service/types.rs b/crates/service_utils/src/service/types.rs similarity index 99% rename from crates/service-utils/src/service/types.rs rename to crates/service_utils/src/service/types.rs index 448dd63fa..5f0db5eb3 100644 --- a/crates/service-utils/src/service/types.rs +++ b/crates/service_utils/src/service/types.rs @@ -32,7 +32,6 @@ pub struct AppState { pub app_env: AppEnv, pub tenants: HashSet<String>, pub cac_version: String, - pub admin_token: String, pub db_pool: PgSchemaManager, pub default_config_validation_schema: JSONSchema, pub meta_schema: JSONSchema, diff --git a/crates/superposition/Cargo.toml b/crates/superposition/Cargo.toml new file mode 100644 index 000000000..3b1997bed --- /dev/null +++ b/crates/superposition/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "superposition" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cac_client = { path = "../cac_client" } +frontend = { path = "../frontend" } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +context_aware_config = { path = "../context_aware_config" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +strum_macros = { workspace = true } +strum = { workspace = true } +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +blake3 = { workspace = true} +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +rand = { workspace = true } +tracing-log = "0.1.3" +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" +futures = "0.3.28" +actix-http = "3.3.1" +futures-util = "0.3.28" +actix-cors = "0.6.4" +leptos_actix = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +actix-files = { version = "0.6" } +anyhow = { workspace = true } diff --git a/crates/superposition/src/main.rs b/crates/superposition/src/main.rs new file mode 100644 index 000000000..1975d1b69 --- /dev/null +++ b/crates/superposition/src/main.rs @@ -0,0 +1,246 @@ +use actix_web::dev::Service; +use actix_web::HttpMessage; +use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; +use context_aware_config::api::*; +use context_aware_config::helpers::{ + get_default_config_validation_schema, get_meta_schema, +}; +use dotenv; +use experimentation_platform::api::*; +use std::{collections::HashSet, io::Result}; +use superposition_types::User; + +use snowflake::SnowflakeIdGenerator; +use std::{sync::Mutex, time::Duration}; + +use actix_files::Files; +use frontend::app::*; +use frontend::types::Envs as UIEnvs; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; +use service_utils::{ + db::pgschema_manager::PgSchemaManager, + db::utils::init_pool_manager, + helpers::{get_from_env_or_default, get_from_env_unsafe}, + middlewares::{ + app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, + }, + service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, +}; + +#[actix_web::get("favicon.ico")] +async fn favicon( + leptos_options: actix_web::web::Data<leptos::LeptosOptions>, +) -> actix_web::Result<actix_files::NamedFile> { + let leptos_options = leptos_options.into_inner(); + let site_root = &leptos_options.site_root; + Ok(actix_files::NamedFile::open(format!( + "{site_root}/favicon.ico" + ))?) +} + +#[actix_web::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + env_logger::init(); + let service_prefix: String = + get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set"); + + /* + Reading from a env returns a String at best we cannot obtain a &'static str from it, + which seems logical as it not known at compiletime, and there is no straightforward way to do this. + + Leptos' Router component base prop type is &'static str, since service_prefix is of String type + we cannot give this as base value. + + This can be solved, if somehow we can tell rust that this String is going to live for entirety of the process, + here comes Box::leak() to our rescue, which keeps the value in the memory for the entire process lifetime, + this also enables to borrow the String value as &'static str . + */ + let service_prefix_str: &'static str = Box::leak(service_prefix.into_boxed_str()); + let base = match service_prefix_str { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + + let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); + let cac_port: u16 = get_from_env_unsafe("PORT").unwrap_or(8080); + let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") + .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); + + let api_host: String = + get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); + let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); + let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") + .expect("ENABLE_TENANT_AND_SCOPE is not set"); + let tenants: HashSet<String> = get_from_env_unsafe::<String>("TENANTS") + .expect("TENANTS is not set") + .split(",") + .map(|tenant| tenant.to_string()) + .collect::<HashSet<String>>(); + let tenant_middleware_exclusion_list = + get_from_env_unsafe::<String>("TENANT_MIDDLEWARE_EXCLUSION_LIST") + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(",") + .map(String::from) + .collect::<HashSet<String>>(); + + let string_to_int = |s: &String| -> i32 { + s.chars() + .map(|i| (i as i32) & rand::random::<i32>()) + .fold(0, i32::wrapping_add) + }; + + let schema_manager: PgSchemaManager = init_pool_manager( + tenants.clone(), + enable_tenant_and_scope, + app_env, + max_pool_size, + ) + .await; + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + let allow_same_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); + let allow_diff_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); + let allow_same_keys_non_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + /* Frontend configurations */ + let ui_redirect_path = match tenants.iter().next() { + Some(tenant) => format!("{}/admin/{}/resolve", base, tenant), + None => String::from("/admin"), + }; + + let ui_envs = UIEnvs { + service_prefix: service_prefix_str, + tenants: tenants.clone().into_iter().collect::<Vec<String>>(), + host: api_host.clone(), + }; + + let routes_ui_envs = ui_envs.clone(); + + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); + // Generate the list of routes in your Leptos App + let routes = generate_route_list(move || { + return view! { <App app_envs=routes_ui_envs.clone()/> }; + }); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + let leptos_envs = ui_envs.clone(); + let cac_host = cac_host.to_owned() + base.as_str(); + App::new() + .wrap_fn(|req, srv| { + let user = User::default(); + req.extensions_mut().insert::<User>(user); + srv.call(req) + }) + .wrap(TenantMiddlewareFactory) + .app_data(Data::new(AppState { + db_pool: schema_manager.clone(), + default_config_validation_schema: get_default_config_validation_schema(), + cac_host: cac_host.to_owned(), + cac_version: cac_version.to_owned(), + + experimentation_flags: ExperimentationFlags { + allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx + .to_owned(), + allow_diff_keys_overlapping_ctx: allow_diff_keys_overlapping_ctx + .to_owned(), + allow_same_keys_non_overlapping_ctx: + allow_same_keys_non_overlapping_ctx.to_owned(), + }, + + snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1,1)), + meta_schema: get_meta_schema(), + app_env: app_env.to_owned(), + enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), + tenants: tenants.to_owned(), + tenant_middleware_exclusion_list: tenant_middleware_exclusion_list + .to_owned(), + service_prefix: service_prefix_str.to_owned(), + })) + .wrap( + actix_web::middleware::DefaultHeaders::new() + .add(("X-SERVER-VERSION", cac_version.to_string())) + ) + .service(web::redirect("/", ui_redirect_path.to_string())) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + move || view! { <App app_envs=leptos_envs.clone()/> }, + ) + .service( + scope(&base) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) + .service( + experiments::endpoints(scope("/experiments")).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) + /***************************** UI Routes ******************************/ + .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}"))) + // serve the favicon from /favicon.ico + ) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + .app_data(Data::new(leptos_options.to_owned())) + }) + .bind(("0.0.0.0", cac_port))? + .workers(5) + .keep_alive(Duration::from_secs( + get_from_env_unsafe("ACTIX_KEEP_ALIVE").unwrap_or(120), + )) + .run() + .await +} diff --git a/makefile b/makefile index 9c63f037c..0502707c1 100644 --- a/makefile +++ b/makefile @@ -21,8 +21,8 @@ cleanup: -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") db-init: - diesel migration run --locked-schema --config-file=crates/context-aware-config/diesel.toml - -diesel migration run --locked-schema --config-file=crates/experimentation-platform/diesel.toml + diesel migration run --locked-schema --config-file=crates/context_aware_config/diesel.toml + -diesel migration run --locked-schema --config-file=crates/experimentation_platform/diesel.toml cac-migration: cleanup docker-compose up -d postgres @@ -32,7 +32,7 @@ cac-migration: cleanup do echo "waiting for postgres bootup"; \ sleep 0.5; \ done - diesel migration run --config-file=crates/context-aware-config/diesel.toml + diesel migration run --config-file=crates/context_aware_config/diesel.toml docker-compose down exp-migration: cleanup @@ -43,7 +43,7 @@ exp-migration: cleanup do echo "waiting for postgres bootup"; \ sleep 0.5; \ done - diesel migration run --config-file=crates/experimentation-platform/diesel.toml + diesel migration run --config-file=crates/experimentation_platform/diesel.toml docker-compose down migration: cac-migration exp-migration @@ -94,18 +94,27 @@ setup: migration env-setup test-tenant dev-tenant # NOTE: The container spinned up are stopped and removed after the work is done. kill: - -pkill -f target/debug/context-aware-config & + -pkill -f target/debug/juspay_superposition & get-password: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh` && echo $$DB_PASSWORD -cac: +juspay_superposition: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ - cargo run --color always --bin context-aware-config --no-default-features --features=ssr + cargo run --color always --bin juspay_superposition --no-default-features --features=ssr -dev: +juspay_superposition_dev: export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ - cargo watch -x 'run --color always --bin context-aware-config --no-default-features --features=ssr' + cargo watch -x 'run --color always --bin juspay_superposition --no-default-features --features=ssr' + +superposition: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo run --color always --bin superposition --no-default-features --features=ssr + +superposition_dev: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo watch -x 'run --color always --bin superposition --no-default-features --features=ssr' + frontend: cd crates/frontend && \ @@ -119,20 +128,28 @@ frontend: backend: -rm -rf target/node_modules - npm --prefix ./crates/context-aware-config/ ci - mv crates/context-aware-config/node_modules target/ + npm --prefix ./crates/context_aware_config/ ci + mv crates/context_aware_config/node_modules target/ cargo build --color always build: frontend backend run: kill build + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + make superposition -e DOCKER_DNS=$(DOCKER_DNS) + +juspay_run: kill build while ! make validate-psql-connection validate-aws-connection; \ do echo "waiting for postgres, localstack bootup"; \ sleep 0.5; \ done cp .env.example .env sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env - make cac -e DOCKER_DNS=$(DOCKER_DNS) + make juspay_superposition -e DOCKER_DNS=$(DOCKER_DNS) ci-test: cleanup ci-setup cargo test diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh index fb3d48f11..913d5c423 100755 --- a/scripts/create-tenant.sh +++ b/scripts/create-tenant.sh @@ -32,8 +32,8 @@ function generate_sql() { psql "$DB_URL" -f ${schema}.sql } -generate_sql "context-aware-config" $CAC_SCHEMA -generate_sql "experimentation-platform" $EXP_SCHEMA +generate_sql "context_aware_config" $CAC_SCHEMA +generate_sql "experimentation_platform" $EXP_SCHEMA psql "$DB_URL" -c "INSERT INTO $CAC_SCHEMA.dimensions (dimension, priority, created_at, created_by, schema, function_name) VALUES ('variantIds', 1, CURRENT_TIMESTAMP, 'anon@juspay.in', '{\"type\": \"string\",\"pattern\": \".*\"}'::json, null);" shopt -u extglob \ No newline at end of file diff --git a/scripts/legacy-db-setup.sh b/scripts/legacy-db-setup.sh index c7884c248..35380e805 100755 --- a/scripts/legacy-db-setup.sh +++ b/scripts/legacy-db-setup.sh @@ -1,17 +1,17 @@ DB_URL=$1 -cp -r "crates/context-aware-config/migrations/." "crates/context-aware-config/cac_v1_migrations" -find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; +cp -r "crates/context_aware_config/migrations/." "crates/context_aware_config/cac_v1_migrations" +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; -find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec cat {} \; +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec cat {} \; -xargs cp -r "crates/experimentation-platform/migrations/." "crates/experimentation-platform/cac_v1_migrations" -find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; +xargs cp -r "crates/experimentation_platform/migrations/." "crates/experimentation_platform/cac_v1_migrations" +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; -find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec cat {} \; +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec cat {} \; -find "crates/context-aware-config/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; -find "crates/experimentation-platform/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; -rm -rf "crates/context-aware-config/cac_v1_migrations" -rm -rf "crates/experimentation-platform/cac_v1_migrations" \ No newline at end of file +rm -rf "crates/context_aware_config/cac_v1_migrations" +rm -rf "crates/experimentation_platform/cac_v1_migrations" \ No newline at end of file From 189986dadb911fca236b741949171c81275ed646 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:28:06 +0000 Subject: [PATCH 352/352] chore(deps-dev): bump axios from 0.16.2 to 0.28.0 Bumps [axios](https://github.com/axios/axios) from 0.16.2 to 0.28.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.16.2...v0.28.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 42 ++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6d2e5d48..4924571ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "context-aware-configuration", "version": "0.0.1", "devDependencies": { - "axios": "^0.16.1", + "axios": "^0.28.0", "daisyui": "^4.3.1", - "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", + "newman": "https://github.com/knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5" } }, @@ -283,14 +283,14 @@ "dev": true }, "node_modules/axios": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", - "integrity": "sha512-IMYFDrcVbUksQhsMYtWCM6KdNaDpr1NY56dpzaIgj92ecPVI29bf2sOgAf8aGTiq8UoixJD61Pj0Ahej5DPv7w==", - "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", "dev": true, "dependencies": { - "follow-redirects": "^1.2.3", - "is-buffer": "^1.1.5" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -891,6 +891,20 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1120,12 +1134,6 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -1927,6 +1935,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 24a14886c..5cf3ede2a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "load_cac_tests": "npx newman dir-import postman/cac -o postman/cac.postman_collection.json" }, "devDependencies": { - "axios": "^0.16.1", + "axios": "^0.28.0", "daisyui": "^4.3.1", "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", "tailwindcss": "^3.3.5"